// @ts-check

// VAST support module

/* Type declarations */

/**
 * @typedef {Object} RawAdTree
 * @property {Array<RawAdTree>} children
 * @property {XMLDocument} data
 * @property {'inLine'|'wrapper'} tagType
 * @property {boolean|undefined} fallbackOnNoAd
 * @property {Array<XMLDocument> | undefined} wrappers
*/

/**
 * @typedef {Object} RawAd
 * @property {XMLDocument} data
 * @property {Array<XMLDocument>} wrappers
 * @property {'inLine' | 'wrapper'} tagType
 */

/**
 * @typedef {Object & RawAd} Ad
 * @property {Array<string>} clicktracking
 * @property {string} errorUrl
 * @property {Array<string>} impressions
 * @property {Array<string>} viewImpression
 * @property {Array<any>} stopTracking
 * @property {Array<any>} tracking
 * @property {number|null} sequence
 * @property {number} duration
 * @property {boolean} played
 */

/**
 *
 * @param {import("../fluidplayer.js").FluidPlayer} playerInstance
 * @param {unknown} options
 */
function vast(playerInstance, options) {
    /**
     * Gets CTA parameters from VAST and sets them on tempOptions
     *
     * Fallbacks to any value that is filled on the TitleCTA extension, but needs at least an url and a text
     *
     * @param {HTMLElement} titleCtaElement
     *
     * @param {any} tmpOptions
     */
    playerInstance.setCTAFromVast = (titleCtaElement, tmpOptions) => {
        if (playerInstance.displayOptions.vastOptions.adCTATextVast && titleCtaElement) {
            const mobileText = playerInstance.extractNodeDataByTagName(titleCtaElement, 'MobileText');
            const desktopText = playerInstance.extractNodeDataByTagName(titleCtaElement, 'PCText');
            const link =
                playerInstance.extractNodeDataByTagName(titleCtaElement, 'DisplayUrl') ||
                playerInstance.extractNodeDataByTagName(titleCtaElement, 'Link');
            const tracking = playerInstance.extractNodeDataByTagName(titleCtaElement, 'Tracking');
            const isMobile = window.matchMedia('(max-width: 768px)').matches;

            if ((desktopText || mobileText) && tracking) {
                tmpOptions.titleCTA = {
                    text: isMobile ?
                        mobileText || desktopText :
                        desktopText || mobileText,
                    link: link || null,
                    tracking,
                }
            }
        }
    };

    playerInstance.getClickThroughUrlFromLinear = (linear) => {
        const videoClicks = linear.getElementsByTagName('VideoClicks');

        if (videoClicks.length) { //There should be exactly 1 node
            const clickThroughs = videoClicks[0].getElementsByTagName('ClickThrough');

            if (clickThroughs.length) {
                return playerInstance.extractNodeData(clickThroughs[0]);
            }
        }

        return false;
    };

    playerInstance.getVastAdTagUriFromWrapper = (xmlResponse) => {
        const wrapper = xmlResponse.getElementsByTagName('Wrapper');

        if (typeof wrapper !== 'undefined' && wrapper.length) {
            const vastAdTagURI = wrapper[0].getElementsByTagName('VASTAdTagURI');

            if (vastAdTagURI.length) {
                return playerInstance.extractNodeData(vastAdTagURI[0]);
            }
        }

        return false;
    };

    playerInstance.hasInLine = (xmlResponse) => {
        const inLine = xmlResponse.getElementsByTagName('InLine');
        return ((typeof inLine !== 'undefined') && inLine.length);
    };

    playerInstance.hasVastAdTagUri = (xmlResponse) => {
        const vastAdTagURI = xmlResponse.getElementsByTagName('VASTAdTagURI');
        return ((typeof vastAdTagURI !== 'undefined') && vastAdTagURI.length);
    };

    playerInstance.getClickThroughUrlFromNonLinear = (nonLinear) => {
        let result = '';
        const nonLinears = nonLinear.getElementsByTagName('NonLinear');

        if (nonLinears.length) {//There should be exactly 1 node
            const nonLinearClickThrough = nonLinear.getElementsByTagName('NonLinearClickThrough');
            if (nonLinearClickThrough.length) {
                result = playerInstance.extractNodeData(nonLinearClickThrough[0]);
            }
        }

        return result;
    };

    playerInstance.getTrackingFromLinear = (linear) => {
        const trackingEvents = linear.getElementsByTagName('TrackingEvents');

        if (trackingEvents.length) {//There should be no more than one node
            return trackingEvents[0].getElementsByTagName('Tracking');
        }

        return [];
    };

    playerInstance.getDurationFromLinear = (linear) => {
        const duration = linear.getElementsByTagName('Duration');

        if (duration.length && (typeof duration[0].childNodes[0] !== 'undefined')) {
            const nodeDuration = playerInstance.extractNodeData(duration[0]);
            return playerInstance.convertTimeStringToSeconds(nodeDuration);
        }

        return false;
    };

    playerInstance.getDurationFromNonLinear = (tag) => {
        let result = 0;
        const nonLinear = tag.getElementsByTagName('NonLinear');
        if (nonLinear.length && (typeof nonLinear[0].getAttribute('minSuggestedDuration') !== 'undefined')) {
            result = playerInstance.convertTimeStringToSeconds(nonLinear[0].getAttribute('minSuggestedDuration'));
        }
        return result;
    };

    playerInstance.getDimensionFromNonLinear = (tag) => {
        const result = { 'width': null, 'height': null };
        const nonLinear = tag.getElementsByTagName('NonLinear');

        if (nonLinear.length) {
            if (typeof nonLinear[0].getAttribute('width') !== 'undefined') {
                result.width = nonLinear[0].getAttribute('width');
            }
            if (typeof nonLinear[0].getAttribute('height') !== 'undefined') {
                result.height = nonLinear[0].getAttribute('height');
            }
        }

        return result;
    };

    playerInstance.getCreativeTypeFromStaticResources = (tag) => {
        let result = '';
        const nonLinears = tag.getElementsByTagName('NonLinear');

        if (nonLinears.length && (typeof nonLinears[0].childNodes[0] !== 'undefined')) {//There should be exactly 1 StaticResource node
            result = nonLinears[0].getElementsByTagName('StaticResource')[0].getAttribute('creativeType');
        }

        return result.toLowerCase();
    };

    playerInstance.getMediaFilesFromLinear = (linear) => {
        const mediaFiles = linear.getElementsByTagName('MediaFiles');

        if (mediaFiles.length) {//There should be exactly 1 MediaFiles node
            return mediaFiles[0].getElementsByTagName('MediaFile');
        }

        return [];
    };

    playerInstance.getStaticResourcesFromNonLinear = (linear) => {
        let result = [];
        const nonLinears = linear.getElementsByTagName('NonLinear');

        if (nonLinears.length) {//There should be exactly 1 StaticResource node
            result = nonLinears[0].getElementsByTagName('StaticResource');
        }

        return result;
    };

    /**
     * Gets the first element found by tag name, and returns the element data
     *
     * @param {HTMLElement} parentNode
     *
     * @param {string} tagName
     *
     * @returns {string|null}
     */
    playerInstance.extractNodeDataByTagName = (parentNode, tagName) => {
        const element = parentNode.getElementsByTagName(tagName);

        if (element && element.length) {
            return playerInstance.extractNodeData(element[0]);
        } else {
            return null;
        }
    };

    playerInstance.extractNodeData = (parentNode) => {
        let contentAsString = "";
        for (let n = 0; n < parentNode.childNodes.length; n++) {
            const child = parentNode.childNodes[n];
            if (child.nodeType === 8 || (child.nodeType === 3 && /^\s*$/.test(child.nodeValue))) {
                // Comments or text with no content
            } else {
                contentAsString += child.nodeValue;
            }
        }
        return contentAsString.replace(/(^\s+|\s+$)/g, '');
    };

    playerInstance.getAdParametersFromLinear = (linear) => {
        const adParameters = linear.getElementsByTagName('AdParameters');
        let adParametersData = null;

        if (adParameters.length) {
            adParametersData = playerInstance.extractNodeData(adParameters[0]);
        }

        return adParametersData;
    };

    playerInstance.getMediaFileListFromLinear = (linear) => {
        const mediaFileList = [];
        const mediaFiles = playerInstance.getMediaFilesFromLinear(linear);

        if (!mediaFiles.length) {
            return mediaFileList;
        }

        for (let n = 0; n < mediaFiles.length; n++) {
            let mediaType = mediaFiles[n].getAttribute('mediaType');

            if (!mediaType) {
                // if there is no mediaType attribute then the video is 2D
                mediaType = '2D';
            }

            // get all the attributes of media file
            mediaFileList.push({
                'src': playerInstance.extractNodeData(mediaFiles[n]),
                'type': mediaFiles[n].getAttribute('type'),
                'apiFramework': mediaFiles[n].getAttribute('apiFramework'),
                'codec': mediaFiles[n].getAttribute('codec'),
                'id': mediaFiles[n].getAttribute('codec'),
                'fileSize': mediaFiles[n].getAttribute('fileSize'),
                'delivery': mediaFiles[n].getAttribute('delivery'),
                'width': mediaFiles[n].getAttribute('width'),
                'height': mediaFiles[n].getAttribute('height'),
                'mediaType': mediaType.toLowerCase()
            });

        }

        return mediaFileList;
    };

    playerInstance.getIconClickThroughFromLinear = (linear) => {
        const iconClickThrough = linear.getElementsByTagName('IconClickThrough');

        if (iconClickThrough.length) {
            return playerInstance.extractNodeData(iconClickThrough[0]);
        }

        return '';
    };

    playerInstance.getStaticResourceFromNonLinear = (linear) => {
        let fallbackStaticResource;
        const staticResources = playerInstance.getStaticResourcesFromNonLinear(linear);

        for (let i = 0; i < staticResources.length; i++) {
            if (!staticResources[i].getAttribute('type')) {
                fallbackStaticResource = playerInstance.extractNodeData(staticResources[i]);
            }

            if (staticResources[i].getAttribute('type') === playerInstance.displayOptions.staticResource) {
                return playerInstance.extractNodeData(staticResources[i]);
            }
        }

        return fallbackStaticResource;
    };

    playerInstance.registerTrackingEvents = (creativeLinear, tmpOptions) => {
        const trackingEvents = playerInstance.getTrackingFromLinear(creativeLinear);
        let eventType = '';
        let oneEventOffset = 0;

        for (let i = 0; i < trackingEvents.length; i++) {
            eventType = trackingEvents[i].getAttribute('event');

            switch (eventType) {
                case 'start':
                case 'firstQuartile':
                case 'midpoint':
                case 'thirdQuartile':
                case 'complete':
                    if (typeof tmpOptions.tracking[eventType] === 'undefined') {
                        tmpOptions.tracking[eventType] = [];
                    }

                    if (typeof tmpOptions.stopTracking[eventType] === 'undefined') {
                        tmpOptions.stopTracking[eventType] = [];
                    }
                    tmpOptions.tracking[eventType].push(trackingEvents[i].textContent.trim());
                    tmpOptions.stopTracking[eventType] = false;

                    break;

                case 'progress':
                    if (typeof tmpOptions.tracking[eventType] === 'undefined') {
                        tmpOptions.tracking[eventType] = [];
                    }

                    oneEventOffset = playerInstance.convertTimeStringToSeconds(trackingEvents[i].getAttribute('offset'));

                    if (typeof tmpOptions.tracking[eventType][oneEventOffset] === 'undefined') {
                        tmpOptions.tracking[eventType][oneEventOffset] = {
                            elements: [],
                            stopTracking: false
                        };
                    }

                    tmpOptions.tracking[eventType][oneEventOffset].elements.push(trackingEvents[i].textContent.trim());

                    break;

                default:
                    break;
            }
        }
    };

    playerInstance.registerClickTracking = (clickTrackingTag, tmpOptions) => {
        if (!clickTrackingTag || !clickTrackingTag.length) {
            return;
        }

        for (let i = 0; i < clickTrackingTag.length; i++) {
            if (clickTrackingTag[i] === '') {
                continue;
            }

            tmpOptions.clicktracking.push(clickTrackingTag[i]);
        }

    };

    playerInstance.registerViewableImpressionEvents = (viewableImpressionTags, tmpOptions) => {
        if (!viewableImpressionTags.length) {
            return;
        }

        for (let i = 0; i < viewableImpressionTags.length; i++) {
            const viewableImpressionEvent = playerInstance.extractNodeData(viewableImpressionTags[i]);
            tmpOptions.viewImpression.push(viewableImpressionEvent);
        }
    };

    playerInstance.registerImpressionEvents = (impressionTags, tmpOptions) => {
        if (!impressionTags.length) {
            return;
        }

        for (let i = 0; i < impressionTags.length; i++) {
            const impressionEvent = playerInstance.extractNodeData(impressionTags[i]);
            tmpOptions.impression.push(impressionEvent);
        }
    };

    playerInstance.registerErrorEvents = (errorTags, tmpOptions) => {
        if ((typeof errorTags !== 'undefined') &&
            (errorTags !== null) &&
            (errorTags.length === 1) && //Only 1 Error tag is expected
            (errorTags[0].childNodes.length === 1)) {
            tmpOptions.errorUrl = errorTags[0].childNodes[0].nodeValue;
        }
    };

    playerInstance.announceError = (code) => {
        if (typeof playerInstance.vastOptions.errorUrl === 'undefined' || !playerInstance.vastOptions.errorUrl) {
            return;
        }

        const parsedCode = typeof code !== 'undefined' ? parseInt(code) : 900;
        const errorUrl = playerInstance.vastOptions.errorUrl.replace('[ERRORCODE]', parsedCode);

        //Send the error request
        playerInstance.callUris([errorUrl]);
    };

    playerInstance.getClickTrackingEvents = (linear) => {
        const result = [];

        const videoClicks = linear.getElementsByTagName('VideoClicks');

        //There should be exactly 1 node
        if (!videoClicks.length) {
            return;
        }

        const clickTracking = videoClicks[0].getElementsByTagName('ClickTracking');

        if (!clickTracking.length) {
            return;
        }

        for (let i = 0; i < clickTracking.length; i++) {
            const clickTrackingEvent = playerInstance.extractNodeData(clickTracking[i]);
            result.push(clickTrackingEvent);
        }

        return result;
    };

    playerInstance.getNonLinearClickTrackingEvents = (nonLinear) => {
        const result = [];
        const nonLinears = nonLinear.getElementsByTagName('NonLinear');

        if (!nonLinears.length) {
            return;
        }

        const clickTracking = nonLinear.getElementsByTagName('NonLinearClickTracking');

        if (!clickTracking.length) {
            return;
        }

        for (let i = 0; i < clickTracking.length; i++) {
            const NonLinearClickTracking = playerInstance.extractNodeData(clickTracking[i]);
            result.push(NonLinearClickTracking);
        }

        return result;
    };

    // TODO: ???
    playerInstance.callUris = (uris) => {
        for (let i = 0; i < uris.length; i++) {
            new Image().src = uris[i];
        }
    };

    playerInstance.recalculateAdDimensions = () => {
        const videoPlayer = playerInstance.domRef.player;
        const divClickThrough = playerInstance.domRef.wrapper.querySelector('.vast_clickthrough_layer');

        if (divClickThrough) {
            divClickThrough.style.width = videoPlayer.offsetWidth + 'px';
            divClickThrough.style.height = videoPlayer.offsetHeight + 'px';
        }

        const requestFullscreenFunctionNames = playerInstance.checkFullscreenSupport();
        const fullscreenButton = playerInstance.domRef.wrapper.querySelector('.fluid_control_fullscreen');
        const menuOptionFullscreen = playerInstance.domRef.wrapper.querySelector('.context_option_fullscreen');

        if (requestFullscreenFunctionNames) {
            // this will go other way around because we already exited full screen
            if (document[requestFullscreenFunctionNames.isFullscreen] === null) {
                // Exit fullscreen
                playerInstance.fullscreenOff(fullscreenButton, menuOptionFullscreen);
            } else {
                // Go fullscreen
                playerInstance.fullscreenOn(fullscreenButton, menuOptionFullscreen);
            }
        } else {
            // TODO: I am fairly certain this fallback does not work...
            //The browser does not support the Fullscreen API, so a pseudo-fullscreen implementation is used
            const fullscreenTag = playerInstance.domRef.wrapper;

            if (fullscreenTag.className.search(/\bpseudo_fullscreen\b/g) !== -1) {
                fullscreenTag.className += ' pseudo_fullscreen';
                playerInstance.fullscreenOn(fullscreenButton, menuOptionFullscreen);
            } else {
                fullscreenTag.className = fullscreenTag.className.replace(/\bpseudo_fullscreen\b/g, '');
                playerInstance.fullscreenOff(fullscreenButton, menuOptionFullscreen);
            }
        }
    };

    /**
     * Prepares VAST for instant ads
     *
     * @param roll
     */
    playerInstance.prepareVast = (roll) => {
        let list = playerInstance.findRoll(roll);

        for (let i = 0; i < list.length; i++) {
            const rollListId = list[i];

            if (!(playerInstance.rollsById[rollListId].vastLoaded !== true && playerInstance.rollsById[rollListId].error !== true)) {
                continue;
            }

            playerInstance.processVastWithRetries(playerInstance.rollsById[rollListId]);
        }
    };

    playerInstance.playMainVideoWhenVastFails = (errorCode) => {
        playerInstance.debugMessage('playMainVideoWhenVastFails called');
        playerInstance.domRef.player.removeEventListener('loadedmetadata', playerInstance.switchPlayerToVastMode);
        playerInstance.domRef.player.pause();
        playerInstance.toggleLoader(false);
        playerInstance.displayOptions.vastOptions.vastAdvanced.noVastVideoCallback();

        if (!playerInstance.vastOptions || typeof playerInstance.vastOptions.errorUrl === 'undefined') {
            playerInstance.announceLocalError(errorCode);
        } else {
            playerInstance.announceError(errorCode);
        }

        playerInstance.switchToMainVideo();
    };

    // TODO: ???
    playerInstance.switchPlayerToVastMode = () => {
    };

    /**
     * Process the XML response
     *
     * @param ad
     */
    function processAdCreatives(ad) {
        const adElement = ad.data;

        if (!adElement) {
            return;
        }

        const creativeElements = Array.from(adElement.getElementsByTagName('Creative'));

        if (creativeElements.length) {
            for (let i = 0; i < creativeElements.length; i++) {
                const creativeElement = creativeElements[i];

                try {
                    if (ad.adType === 'linear') {
                        const linearCreatives = creativeElement.getElementsByTagName('Linear');
                        const creativeLinear = linearCreatives[0];

                        //Extract the Ad data if it is actually the Ad (!wrapper)
                        if (!playerInstance.hasVastAdTagUri(adElement) && playerInstance.hasInLine(adElement)) {
                            //Set initial values
                            ad.adFinished = false;
                            ad.vpaid = false;

                            //Extract the necessary data from the Linear node
                            ad.skipoffset = playerInstance.convertTimeStringToSeconds(creativeLinear.getAttribute('skipoffset'));
                            ad.clickthroughUrl = playerInstance.getClickThroughUrlFromLinear(creativeLinear);
                            ad.duration = playerInstance.getDurationFromLinear(creativeLinear);
                            ad.mediaFileList = playerInstance.getMediaFileListFromLinear(creativeLinear);
                            ad.adParameters = playerInstance.getAdParametersFromLinear(creativeLinear);
                            ad.iconClick = ad.iconClick || playerInstance.getIconClickThroughFromLinear(creativeLinear);

                            if (ad.adParameters) {
                                ad.vpaid = true;
                            }
                        }
                    }

                    if (ad.adType === 'nonLinear') {
                        const nonLinearCreatives = creativeElement.getElementsByTagName('NonLinearAds');
                        const creativeNonLinear = nonLinearCreatives[0];

                        //Extract the Ad data if it is actually the Ad (!wrapper)
                        if (!playerInstance.hasVastAdTagUri(adElement) && playerInstance.hasInLine(adElement)) {
                            //Set initial values
                            ad.vpaid = false;

                            //Extract the necessary data from the NonLinear node
                            ad.clickthroughUrl = playerInstance.getClickThroughUrlFromNonLinear(creativeNonLinear);
                            ad.duration = playerInstance.getDurationFromNonLinear(creativeNonLinear); // VAST version < 4.0
                            ad.dimension = playerInstance.getDimensionFromNonLinear(creativeNonLinear); // VAST version < 4.0
                            ad.staticResource = playerInstance.getStaticResourceFromNonLinear(creativeNonLinear);
                            ad.creativeType = playerInstance.getCreativeTypeFromStaticResources(creativeNonLinear);
                            ad.adParameters = playerInstance.getAdParametersFromLinear(creativeNonLinear);

                            if (ad.adParameters) {
                                ad.vpaid = true;
                            }
                        }
                    }

                    // Current support is for only one creative element
                    // break the loop if creative was successful
                    break;
                } catch (err) {
                    if (creativeElement.firstElementChild &&
                        !(creativeElement.firstElementChild.tagName === 'Linear' ||
                            creativeElement.firstElementChild.tagName === 'NonLinearAds')) {
                        console.warn('Skipping ' + creativeElement.firstElementChild.tagName + ', this might not be supported yet.')
                    }
                    console.error(err);
                }
            };
        }

        return ad;
    }

    /**
     * Parse the VAST Tag
     *
     * @param vastObj
     */
    playerInstance.processVastWithRetries = (vastObj) => {
        let vastTag = vastObj.vastTag;
        const rollListId = vastObj.id;

        playerInstance.domRef.player.addEventListener('adId_' + rollListId, playerInstance[vastObj.roll]);

        const handleVastResult = function (pass, adOptionsList) {
            if (pass && Array.isArray(adOptionsList) && !playerInstance.displayOptions.vastOptions.allowVPAID && adOptionsList.some(adOptions => adOptions.vpaid)) {
                adOptionsList = adOptionsList.filter(adOptions => adOptions.vpaid !== true);
                playerInstance.announceLocalError('103', 'VPAID not allowed, so skipping this VAST tag.')
            }

            if (pass && Array.isArray(adOptionsList) && adOptionsList.length) {

                playerInstance.adPool[rollListId] = [];

                adOptionsList.forEach((tmpOptions, index) => {
                    tmpOptions.id = rollListId + '_AD' + index;
                    tmpOptions.rollListId = rollListId;

                    if (tmpOptions.adType === 'linear') {

                        if ((typeof tmpOptions.iconClick !== 'undefined') && (tmpOptions.iconClick !== null) && tmpOptions.iconClick.length) {
                            tmpOptions.landingPage = tmpOptions.iconClick;
                        }

                        const selectedMediaFile = playerInstance.getSupportedMediaFileObject(tmpOptions.mediaFileList);
                        if (selectedMediaFile) {
                            tmpOptions.mediaType = selectedMediaFile.mediaType;
                        }

                    }

                    tmpOptions.adType = tmpOptions.adType ? tmpOptions.adType : 'unknown';
                    playerInstance.adPool[rollListId].push(Object.assign({}, tmpOptions));

                    if (playerInstance.hasTitle()) {
                        const title = playerInstance.domRef.wrapper.querySelector('.fp_title');
                        title.style.display = 'none';
                    }

                    playerInstance.rollsById[rollListId].ads.push(tmpOptions);
                });

                playerInstance.rollsById[rollListId].vastLoaded = true;

                const event = document.createEvent('Event');

                event.initEvent('adId_' + rollListId, false, true);
                playerInstance.domRef.player.dispatchEvent(event);
                playerInstance.displayOptions.vastOptions.vastAdvanced.vastLoadedCallback();
            } else {
                // when vast failed
                playerInstance.announceLocalError('101');

                if (vastObj.hasOwnProperty('fallbackVastTags') && vastObj.fallbackVastTags.length > 0) {
                    vastTag = vastObj.fallbackVastTags.shift();
                    playerInstance.processUrl(vastTag, handleVastResult, rollListId);
                } else {
                    if (vastObj.roll === 'preRoll') {
                        playerInstance.preRollFail(vastObj);
                    }
                    playerInstance.rollsById[rollListId].error = true;
                }
            }
        };

        playerInstance.processUrl(vastTag, handleVastResult, rollListId);
    };

    playerInstance.processUrl = (vastTag, callBack, rollListId) => {
        const numberOfRedirects = 0;

        const tmpOptions = {
            tracking: [],
            stopTracking: [],
            impression: [],
            viewImpression: [],
            clicktracking: [],
            vastLoaded: false
        };

        playerInstance.resolveVastTag(
            vastTag,
            numberOfRedirects,
            tmpOptions,
            callBack,
            rollListId
        );
    };

    /**
     * Gets first stand-alone ad
     *
     * @param {Array<RawAdTree>} ads
     * @returns {Array<RawAdTree>}
     */
    function getFirstStandAloneAd(ads) {
        for (const ad of ads) {
            const isAdPod = ad.data.attributes.sequence !== undefined;

            if (!isAdPod) {
                return [ad];
            }
        }

        return [];
    }

    /**
     * Resolves ad requests recursively and returns a tree of "Ad" and "Wrapper" elements
     *
     * @param {string} url vast resource url
     * @param {number} maxDepth depth of recursive calls (wrapper depth)
     * @param {Partial<RawAdTree>} baseNode used for recursive calls as base node
     * @param {number} currentDepth used internally to track depth
     * @param {boolean} followAdditionalWrappers used internally to track nested wrapper calls
     * @returns {Promise<RawAdTree>}
     */
    async function resolveAdTreeRequests(url, maxDepth, baseNode = {}, currentDepth = 0, followAdditionalWrappers = true) {
        const adTree = { ...baseNode, children: [] };
        const { responseXML } = await playerInstance.sendRequestAsync(url, true, playerInstance.displayOptions.vastOptions.vastTimeout);
        const adElements = Array.from(responseXML.getElementsByTagName('Ad'));

        for (const adElement of adElements) {
            const vastAdTagUri = playerInstance.getVastAdTagUriFromWrapper(adElement);
            const isAdPod = adElement.attributes.sequence !== undefined;
            const adNode = { data: adElement };

            if (vastAdTagUri && currentDepth <= maxDepth && followAdditionalWrappers) {
                const [wrapperElement] = adElement.getElementsByTagName('Wrapper');
                const disableAdditionalWrappers = wrapperElement.attributes.followAdditionalWrappers && ["false", "0"].includes(wrapperElement.attributes.followAdditionalWrappers.value); // See VAST Wrapper spec
                const allowMultipleAds = wrapperElement.attributes.allowMultipleAds && ["true", "1"].includes(wrapperElement.attributes.allowMultipleAds.value); // See VAST Wrapper spec
                const fallbackOnNoAd = wrapperElement.attributes.fallbackOnNoAd && ["true", "1"].includes(wrapperElement.attributes.fallbackOnNoAd.value);

                try {
                    const wrapperResponse = await resolveAdTreeRequests(vastAdTagUri, maxDepth, { tagType: 'wrapper', ...adNode, fallbackOnNoAd }, currentDepth+1, !disableAdditionalWrappers);
                    wrapperResponse.fallbackOnNoAd = fallbackOnNoAd;

                    if (!allowMultipleAds || isAdPod) {
                        wrapperResponse.children = getFirstStandAloneAd(wrapperResponse.children);
                    }

                    adTree.children.push(wrapperResponse);
                } catch (e) {
                    adTree.children.push({ tagType: `wrapper`, fallbackOnNoAd, httpError: true })
                    playerInstance.debugMessage(`Error when loading Wrapper, will trigger fallback if available`, e);
                }
            } else if (!vastAdTagUri) {
                let mediaFileIsValid = true;
                let mediaFileUrl = '';
                if (Array.from(adElement.getElementsByTagName('AdParameters')).length) {
                    const mediaFiles = Array.from(adElement.getElementsByTagName('AdParameters'));
                    mediaFileIsValid = false;
                    for (const mediaFile of mediaFiles) {
                        mediaFileUrl = mediaFile.textContent.trim();
                        try {
                            const mediaFileObj = JSON.parse(mediaFileUrl);
                            mediaFileUrl = mediaFileObj.videos[0].url;
                        } catch (error) {
                            console.error("Error parsing media file URL:", error);
                        }
                    }
                } else if (Array.from(adElement.getElementsByTagName('MediaFiles')).length) {
                    const mediaFiles = Array.from(adElement.getElementsByTagName('MediaFiles'));
                    const mediaFile = mediaFiles[0].getElementsByTagName('MediaFile');
                    mediaFileIsValid = false;
                    for (const mediaFileTemp of mediaFile) {
                        mediaFileUrl = mediaFileTemp.textContent.trim();
                    }
                };
                mediaFileIsValid = await validateMediaFile(mediaFileUrl);
                if (mediaFileIsValid) {
                    adTree.children.push({ tagType: 'inLine', mediaFileUrl, ...adNode });
                    break;
                } else {
                    adTree.children.push({ tagType: 'inLine', mediaError: true, ...adNode });
                    playerInstance.debugMessage(`No valid media file found in Inline ad.`);
                }
            }
        }

        return adTree;
    }


    /**
     * Validate Media File to check if videos play
     *
     * @param {mediaFileUrl}
     */
    async function validateMediaFile(mediaFileUrl) {
        try {
            const response =  await fetch(mediaFileUrl);
            if (!response.ok || response.headers.get('content-type').indexOf('video') === -1) {
                return false;
            }
            const videoElement = document.createElement('video');
            videoElement.src = mediaFileUrl;
            const canPlay = await videoElement.canPlayType(response.headers.get('content-type'));
            return canPlay !== "";
        } catch (error) {
            console.error('Failed to load media file:', error);
            return false;
        }
    }

    /**
     * Transforms an Ad Tree to a 1-dimensional array of Ads with wrapper data attached to each ad
     *
     * @param {RawAdTree} root
     * @param {Array<RawAd>} ads
     * @param {Array<XMLDocument>} wrappers
     * @returns {Array<RawAd>}
     */
    function flattenAdTree(root, ads = [], wrappers = []) {
        const currentWrappers = [...wrappers, root.data];

        if (Array.isArray(root.children) && root.children.length) {
            root.children.forEach(child => flattenAdTree(child, ads, currentWrappers));
        }

        if (root.tagType === 'inLine') {
            ads.push({ ...root, wrappers: currentWrappers.filter(Boolean) });
        }

        return ads;
    }

    /**
     * Register Ad element properties to an Ad based on its data and its wrapper data if available
     *
     * @param {RawAd} rawAd
     * @param {{ tracking: Array, stopTracking: Array, impression: Array, viewImpression: Array, clicktracking: Array }} options
     * @returns {Ad}
     */
    function registerAdProperties(rawAd, options) {
        const ad = { ...rawAd, ...JSON.parse(JSON.stringify(options)) };

        ad.adType = (ad.data.getElementsByTagName('Linear').length && 'linear') ||
            (ad.data.getElementsByTagName('NonLinearAds').length && 'nonLinear') || 'unknown';

        [...(ad.wrappers || []), ad.data].filter(Boolean).forEach(dataSource => {
            // Register impressions
            const impression = dataSource.getElementsByTagName('Impression');
            if (impression !== null) {
                playerInstance.registerImpressionEvents(impression, ad);
            }

            // Register viewable impressions
            const viewableImpression = dataSource.getElementsByTagName('Viewable');
            if (viewableImpression !== null) {
                playerInstance.registerViewableImpressionEvents(viewableImpression, ad);
            }

            // Get the error tag, if any
            const errorTags = dataSource.getElementsByTagName('Error');
            if (errorTags !== null) {
                playerInstance.registerErrorEvents(errorTags, ad);
            }

            // Sets CTA from vast
            const [titleCta] = dataSource.getElementsByTagName('TitleCTA');
            if (titleCta) {
                playerInstance.setCTAFromVast(titleCta, ad);
            }

            // Register tracking events
            playerInstance.registerTrackingEvents(dataSource, ad);
            const clickTracks = ad.adType === 'linear' ?
                playerInstance.getClickTrackingEvents(dataSource) :
                playerInstance.getNonLinearClickTrackingEvents(dataSource);
            playerInstance.registerClickTracking(clickTracks, ad);
        });

        ad.sequence = ad.data.attributes.sequence ? Number(ad.data.attributes.sequence.value) : null;
        ad.played = false;

        return ad;
    }

    /**
     * Handles selection of ad pod or standalone ad to be played
     *
     * @param {Array<Ad>} ads
     * @param {number} maxDuration
     * @param {number} maxQuantity
     * @param {boolean} forceStandAloneAd
     */
    function getPlayableAds(ads, maxDuration, maxQuantity, forceStandAloneAd) {
        const { adPod } = ads
            .filter(ad => Boolean(ad.sequence))
            .sort((adX, adY) => adX.sequence - adY.sequence)
            .reduce((playableAds, ad) => {
                if (playableAds.adPod.length < maxQuantity && (playableAds.totalDuration + ad.duration) <= maxDuration) {
                    playableAds.adPod.push(ad);
                }

                return playableAds;
            }, { adPod: [], totalDuration: 0 });
        const adBuffet = ads.filter(ad => !Boolean(ad.sequence) && ad.duration < maxDuration);

        const isValidAdPodFormats = adPod.map(ad => ad.adType).slice(0, -1).every(adType => adType === 'linear');

        if (adPod.length > 0 && !forceStandAloneAd && isValidAdPodFormats) {
            playerInstance.debugMessage('Playing valid adPod', adPod);
            return adPod;
        } else {
            playerInstance.debugMessage('Trying to play single ad, adBuffet:', adBuffet);
            return adBuffet.length > 0 ? [adBuffet[0]] : [];
        }
    }

    /**
     * @param vastTag
     * @param numberOfRedirects
     * @param tmpOptions
     * @param callback
     * @param rollListId
     */
    playerInstance.resolveVastTag = (vastTag, numberOfRedirects, tmpOptions, callback, rollListId) => {
        if (!vastTag || vastTag === '') {
            return callback(false);
        }

        resolveAdTreeRequests(vastTag, playerInstance.displayOptions.vastOptions.maxAllowedVastTagRedirects)
            .then(result => {
                try {
                    /** @see VAST 4.0 Wrapper.fallbackOnNoAd */
                    const triggerFallbackOnNoAd = result.children.some(ad =>
                        ad.tagType === 'wrapper' && ad.fallbackOnNoAd && (!/"tagType":"ad"/.test(JSON.stringify(ad)) || ad.httpError)
                    );

                    if (triggerFallbackOnNoAd) {
                        playerInstance.debugMessage('Error on VAST Wrapper, triggering fallbackOnNoAd. Ad tree:', result);
                    }

                    result = flattenAdTree(result).map(ad => processAdCreatives(registerAdProperties(ad, tmpOptions)));

                    const playableAds = getPlayableAds(
                        result,
                        playerInstance.rollsById[rollListId].maxTotalDuration || Number.MAX_SAFE_INTEGER,
                        playerInstance.rollsById[rollListId].maxTotalQuantity || Number.MAX_SAFE_INTEGER,
                        triggerFallbackOnNoAd,
                    );

                    (playableAds && playableAds.length) ? callback(true, playableAds) : callback(false);
                } catch (error) {
                    callback(false);
                }
            })
            .catch(() => {
                return callback(false);
            });
    };

    playerInstance.setVastList = () => {
        const rolls = {};
        const rollsGroupedByType = { preRoll: [], postRoll: [], midRoll: [], onPauseRoll: [] };
        const def = {
            id: null,
            roll: null,
            vastLoaded: false,
            error: false,
            adText: null,
            adTextPosition: null,
        };
        let idPart = 0;

        const validateVastList = function (item) {
            let hasError = false;

            if (item.roll === 'midRoll') {
                if (typeof item.timer === 'undefined') {
                    hasError = true;
                }
            }

            return hasError;
        };

        const validateRequiredParams = function (item) {
            let hasError = false;

            if (!item.vastTag) {
                playerInstance.announceLocalError(102, '"vastTag" property is missing from adList.');
                hasError = true;
            }

            if (!item.roll) {
                playerInstance.announceLocalError(102, '"roll" is missing from adList.');
                hasError = true;
            }

            if (playerInstance.availableRolls.indexOf(item.roll) === -1) {
                playerInstance.announceLocalError(102, 'Only ' + playerInstance.availableRolls.join(',') + ' rolls are supported.');
                hasError = true;
            }

            if (item.size && playerInstance.supportedNonLinearAd.indexOf(item.size) === -1) {
                playerInstance.announceLocalError(102, 'Only ' + playerInstance.supportedNonLinearAd.join(',') + ' size are supported.');
                hasError = true;
            }

            return hasError;
        };

        if (playerInstance.displayOptions.vastOptions.hasOwnProperty('adList')) {

            for (let key in playerInstance.displayOptions.vastOptions.adList) {

                let rollItem = playerInstance.displayOptions.vastOptions.adList[key];

                if (validateRequiredParams(rollItem)) {
                    playerInstance.announceLocalError(102, 'Wrong adList parameters.');
                    continue;
                }
                const id = 'ID' + idPart;

                rolls[id] = Object.assign({}, def);
                rolls[id] = Object.assign(rolls[id], playerInstance.displayOptions.vastOptions.adList[key]);
                if (rollItem.roll === 'midRoll') {
                    rolls[id].error = validateVastList('midRoll', rollItem);
                }
                rolls[id].id = id;
                rolls[id].ads = [];
                idPart++;

            }
        }

        // group the ads by roll
        // pushing object references and forming json
        Object.keys(rolls).map(function (e) {
            switch (rolls[e].roll.toLowerCase()) {
                case 'preRoll'.toLowerCase():
                    rollsGroupedByType.preRoll.push(rolls[e]);
                    break;
                case 'midRoll'.toLowerCase():
                    rollsGroupedByType.midRoll.push(rolls[e]);
                    break;
                case 'postRoll'.toLowerCase():
                    rollsGroupedByType.postRoll.push(rolls[e]);
                    break;
                case 'onPauseRoll'.toLowerCase():
                    rollsGroupedByType.onPauseRoll.push(rolls[e]);
                    break;
                default:
                    console.error(`${rolls[e].roll.toLowerCase()} is not a recognized roll`);
                    break;
            }
        });

        playerInstance.adGroupedByRolls = rollsGroupedByType;
        playerInstance.rollsById = rolls;
    };

    playerInstance.onVastAdEnded = (event) => {
        if (event) {
            event.stopImmediatePropagation();
        }
        playerInstance.vastOptions.adFinished = true;
        //"this" is the HTML5 video tag, because it dispatches the "ended" event
        playerInstance.deleteVastAdElements();
        playerInstance.checkForNextAd();
    };

    playerInstance.vastLogoBehaviour = (vastPlaying) => {
        if (!playerInstance.displayOptions.layoutControls.logo.showOverAds) {
            const logoHolder = playerInstance.domRef.wrapper.querySelector('.logo_holder');

            if (!logoHolder) {
                return;
            }

            logoHolder.style.display = vastPlaying ? 'none' : 'inline';
        }
    };

    playerInstance.deleteVastAdElements = () => {
        playerInstance.removeClickthrough();
        playerInstance.removeSkipButton();
        playerInstance.removeAdCountdown();
        playerInstance.removeAdPlayingText();
        playerInstance.removeCTAButton();
        playerInstance.vastLogoBehaviour(false);
    };
}

export default vast
