public static CreativeCompanions CreateCompanionAds(FWAd source, FWAdReference reference)
        {
            var result = new CreativeCompanions();

            foreach (var creative in source.Creatives)
            {
                var companion = new Companion();
                result.Companions.Add(companion);
                companion.AdSlotId = reference.SlotId ?? string.Empty;

                var rendition = creative.CreativeRenditions.OrderByDescending(cr => cr.Preference).FirstOrDefault(c => c.Asset.MimeType.ToLowerInvariant().StartsWith("image/"));
                if (rendition != null)
                {
                    var asset = rendition.Asset;
                    if (asset != null)
                    {
                        companion.Item = new StaticResource()
                        {
                            Value = new Uri(asset.Url),
                            CreativeType = asset.MimeType
                        };
                        companion.Width = rendition.Width;
                        companion.Height = rendition.Height;
                        companion.Id = asset.Id ?? string.Empty;
                    }
                }

                var allCallbacks = reference.EventCallbacks;
                foreach (var url in allCallbacks.Where(ec => ec.Type == FWCallbackType.Click && !ec.ShowBrowser).SelectMany(ec => ec.GetUrls()))
                {
                    companion.ClickTracking.Add(url);
                }

                var clickUrl = allCallbacks.Where(ec => ec.Type == FWCallbackType.Click && ec.ShowBrowser).SelectMany(ec => ec.GetUrls()).FirstOrDefault();
                if (clickUrl != null)
                {
                    companion.ClickThrough = new Uri(clickUrl);
                }
            }
            return result;
        }
        static async Task<Ad> CreateLinearAd(FWAd source, FWAdReference reference)
        {
            var ad = new Ad();

            var allCallbacks = reference.EventCallbacks;
            foreach (var url in allCallbacks.Where(ec => ec.Type == FWCallbackType.Impression && ec.Name == FWEventCallback.DefaultImpression).SelectMany(ec => ec.GetUrls()))
            {
                ad.Impressions.Add(url);
            }

            int index = 0;
            IEnumerable<FWCreative> creatives = source.Creatives;
            if (reference.CreativeId != null)
            {
                creatives = creatives.Where(c => c.Id == reference.CreativeId);
            }
            foreach (var creative in creatives)
            {
                index++;
                var wrappedAds = new List<Ad>();
                var linear = new CreativeLinear();
                linear.AdParameters = string.Join("&", creative.Parameters.Select(p => Uri.EscapeDataString(p.Name) + "=" + Uri.EscapeDataString(p.Value)));
                linear.Duration = creative.Duration;
                linear.Sequence = index;

                IEnumerable<FWCreativeRendition> creativeRenditions = creative.CreativeRenditions;
                if (reference.CreativeRenditionId != null)
                {
                    creativeRenditions = creativeRenditions.Where(cr => cr.Id == reference.CreativeRenditionId).DefaultIfEmpty(creativeRenditions);
                    if (reference.ReplicaId != null)
                    {
                        creativeRenditions = creativeRenditions.Where(cr => cr.AdReplicaId == reference.ReplicaId).DefaultIfEmpty(creativeRenditions);
                    }
                }

                foreach (var rendition in creativeRenditions)
                {
                    if (!string.IsNullOrEmpty(rendition.WrapperType))
                    {
                        switch (rendition.WrapperType.ToLowerInvariant())
                        {
                            case "external/vast-2":
                                try
                                {
                                    var vastAdUri = new Uri(rendition.WrapperUrl);
                                    // load the stream from the web
                                    using (var s = await Extensions.LoadStreamAsync(vastAdUri))
                                    {
                                        var wrappedVastDoc = await AdModelFactory.CreateFromVast(s, null, true);
                                        if (wrappedVastDoc != null)
                                        {
                                            // use the first ad
                                            var wrappedAd = wrappedVastDoc.AdPods.SelectMany(pod => pod.Ads).FirstOrDefault();
                                            if (wrappedAd != null)
                                            {
                                                wrappedAds.Add(wrappedAd);
                                            }
                                        }
                                    }
                                }
                                catch { /* swallow */ }
                                break;
                        }
                    }
                    else
                    {
                        // TODO: FreeWheel assets can contain Content instead of Url. This could be supported someday; for now it is ignored.
                        if (rendition.Asset != null)
                        {
                            var mediaFile = CreateMediaFile(creative, rendition, rendition.Asset);
                            if (mediaFile != null)
                            {
                                mediaFile.Ranking = (int)rendition.Preference + 1; // add one to indicate this is preferred over "OtherAssets"
                                linear.MediaFiles.Add(mediaFile);
                            }
                        }

                        foreach (var asset in rendition.OtherAssets)
                        {
                            var mediaFile = CreateMediaFile(creative, rendition, asset);
                            if (mediaFile != null)
                            {
                                mediaFile.Ranking = (int)rendition.Preference;
                                linear.MediaFiles.Add(mediaFile);
                            }
                        }
                    }
                }

                // generate callback urls from one base url
                foreach (var eventCallback in allCallbacks.Where(ec => ec.Type == FWCallbackType.Impression))
                {
                    foreach (var url in eventCallback.GetUrls())
                    {
                        switch (eventCallback.Name.ToLower())
                        {
                            case "start":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Start, Value = url });
                                break;
                            case "firstquartile":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.FirstQuartile, Value = url });
                                break;
                            case "midpoint":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Midpoint, Value = url });
                                break;
                            case "thirdquartile":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.ThirdQuartile, Value = url });
                                break;
                            case "complete":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Complete, Value = url });
                                break;
                        }
                    }
                }

                // generate callback urls from one base url
                foreach (var eventCallback in allCallbacks.Where(ec => ec.Type == FWCallbackType.Standard))
                {
                    foreach (var url in eventCallback.GetUrls())
                    {
                        switch (eventCallback.Name.Replace("-", "").ToLower())
                        {
                            case "_creativeview":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.CreativeView, Value = url });
                                break;
                            case "_mute":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Mute, Value = url });
                                break;
                            case "_unmute":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Unmute, Value = url });
                                break;
                            case "_pause":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Pause, Value = url });
                                break;
                            case "_rewind":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Rewind, Value = url });
                                break;
                            case "_resume":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Resume, Value = url });
                                break;
                            case "_fullscreen":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Fullscreen, Value = url });
                                break;
                            case "_exitfullscreen":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.ExitFullscreen, Value = url });
                                break;
                            case "_expand":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Expand, Value = url });
                                break;
                            case "_collapse":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Collapse, Value = url });
                                break;
                            case "_acceptinvitation":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.AcceptInvitation, Value = url });
                                break;
                            case "_close":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Close, Value = url });
                                break;
                            case "_skip":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Skip, Value = url });
                                break;
                            case "_progress":
                                linear.TrackingEvents.Add(new TrackingEvent() { Type = TrackingType.Progress, Value = url });
                                break;
                        }
                    }
                }

                foreach (var url in allCallbacks.Where(ec => ec.Type == FWCallbackType.Click && !ec.ShowBrowser).SelectMany(ec => ec.GetUrls()))
                {
                    linear.ClickTracking.Add(url);
                }

                var clickUrl = allCallbacks.Where(ec => ec.Type == FWCallbackType.Click && ec.ShowBrowser).SelectMany(ec => ec.GetUrls()).FirstOrDefault();
                if (clickUrl != null)
                {
                    linear.ClickThrough = new Uri(clickUrl);
                }

                // generate callback urls from one base url ONLY when the callback does not already exist
                foreach (var eventCallback in allCallbacks.Where(ec => ec.Type == FWCallbackType.Generic))
                {
                    foreach (var url in eventCallback.GetUrls())
                    {
                        var baseUrl = url + string.Format("&metr={0}", FreeWheelFactory.GetSupportedMetrics());

                        // quartile events
                        var quartileUrl = baseUrl + "&ct=[LASTQUARTILE]&et=i"; // [LASTQUARTILE] will get replaced by the VPAID controller
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.FirstQuartile, Value = quartileUrl + "&cn=firstQuartile" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Midpoint, Value = quartileUrl + "&cn=midPoint" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.ThirdQuartile, Value = quartileUrl + "&cn=thirdQuartile" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Complete, Value = quartileUrl + "&cn=complete" });

                        // advanced metrics
                        var advancedUrl = baseUrl + "&et=s";
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Mute, Value = advancedUrl + "&cn=_mute" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Unmute, Value = advancedUrl + "&cn=_un-mute" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Collapse, Value = advancedUrl + "&cn=_collapse" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Expand, Value = advancedUrl + "&cn=_expand" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Pause, Value = advancedUrl + "&cn=_pause" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Resume, Value = advancedUrl + "&cn=_resume" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Rewind, Value = advancedUrl + "&cn=_rewind" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.AcceptInvitation, Value = advancedUrl + "&cn=_accept-invitation" });
                        AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Close, Value = advancedUrl + "&cn=_close" });
                        //AddSecondaryCallback(linear.TrackingEvents, new TrackingEvent() { Type = TrackingType.Minimize, Value = advancedUrl + "&cn=_minimize" });
                    }
                }

                ad.Creatives.Add(linear);

                foreach (var wrappedAd in wrappedAds)
                {
                    AdModelFactory.MergeWrappedAdBeacons(wrappedAd, ad);
                    var wrappedCreative = AdModelFactory.FindMatchingCreative(linear, wrappedAd);
                    AdModelFactory.MergeWrappedCreative(wrappedCreative, linear);
                }
            }

            return ad;
        }
        internal static FWAd LoadAd(XElement element)
        {
            var result = new FWAd();
            result.Id = (string)element.Attribute("adId");
            result.AdUnit = (string)element.Attribute("adUnit");
            result.BundleId = element.GetIntAttribute("bundleId");
            result.NoLoad = element.GetBoolAttribute("noLoad", false);
            result.NoPreload = element.GetBoolAttribute("noPreload", false);

            var creativesXml = element.Element("creatives");
            if (creativesXml != null)
            {
                foreach (var creativeXml in creativesXml.Elements("creative"))
                {
                    result.Creatives.Add(LoadCreative(creativeXml));
                }
            }

            return result;
        }