/// <summary>
        ///     Occurs when the downlaod progress of the video file has changed.
        /// </summary>

        /// <summary>
        ///     Starts the video download.
        /// </summary>
        /// <exception cref="IOException">The video file could not be saved.</exception>
        /// <exception cref="WebException">An error occured while downloading the video.</exception>
        public override void Execute() {
            context.OnProgresStateChanged(YoutubeStage.StartingDownload);
            if (context.VideoInfo==null) throw new InvalidOperationException("Cant extract audio when no VideoInfo is selected.");
            HttpWebRequest request;
            var rpf = new RetryableProcessFailed("Video Downloader") { Tag = context };
            retry:try {
                request = (HttpWebRequest)WebRequest.Create(context.VideoInfo.DownloadUrl);
            } catch (Exception e) {
                rpf.Defaultize(e);
                context.OnDownloadFailed(rpf);
                if (rpf.ShouldRetry)
                    goto retry;
                return;
            }

            if (context.BytesToDownload.HasValue)
                request.AddRange(0, context.BytesToDownload.Value - 1);

            context.VideoPath = new FileInfo(context.VideoPath?.FullName ?? context.VideoSaveableFilename);
            using (var response = request.GetResponse())
            using (var source = response.GetResponseStream())
            using (var target = File.Open(context.VideoPath.FullName, FileMode.Create, FileAccess.Write)) {
                var buffer = new byte[1024];
                var cancel = false;
                int bytes;
                var copiedBytes = 0;

                while (!cancel && (bytes = source.Read(buffer, 0, buffer.Length)) > 0) {
                    target.Write(buffer, 0, bytes);

                    copiedBytes += bytes;

                    var e = context.OnProgresStateChanged(YoutubeStage.Downloading, (copiedBytes * 1.0 / response.ContentLength) * 100f);

                    if (e.Cancel)
                        cancel = true;
                }
            }
            context.OnProgresStateChanged(YoutubeStage.DownloadFinished);
        }
        /// <summary>
        ///     Gets a list of <see cref="VideoInfo" />s for the specified URL.
        /// </summary>
        /// <param name="context">The context, must contain a url to the video in the Url property</param>
        /// <param name="decryptSignature">
        ///     A value indicating whether the video signatures should be decrypted or not. Decrypting
        ///     consists of a HTTP request for each <see cref="VideoInfo" />, so you may want to set
        ///     this to false and call <see cref="DecryptDownloadUrl" /> on your selected
        ///     <see
        ///         cref="VideoInfo" />
        ///     later.
        /// </param>
        /// <returns>A list of <see cref="VideoInfo" />s that can be used to download the video.</returns>
        /// <exception cref="VideoNotAvailableException">The video is not available.</exception>
        /// <exception cref="WebException">
        ///     An error occurred while downloading the YouTube page html.
        /// </exception>
        /// <exception cref="YoutubeParseException">The Youtube page could not be parsed.</exception>
        public static IEnumerable<VideoInfo> GetDownloadUrls(YoutubeContext context, bool decryptSignature = true) {
            if (context == null)
                throw new ArgumentNullException(nameof(context));
            if (context.Url == null)
                throw new ArgumentNullException(nameof(context.Url));
            context.OnProgresStateChanged(YoutubeStage.ProcessingUrls);
            string ytb;
            var isYoutubeUrl = TryNormalizeYoutubeUrl(context.Url, out ytb);
            context.Url = ytb;
            if (!isYoutubeUrl)
                throw new ArgumentException("URL is not a valid youtube URL!");
            _retry:
            try {
                var rpf = new RetryableProcessFailed("ParseHtml5Version") {Tag = context.Url};
                _redownload:
                var json = LoadJson(context.Url);
                var videoTitle = GetVideoTitle(json);
                var n = 0;
                var downloadUrls = ExtractDownloadUrls(json);
                var infos = GetVideoInfos(downloadUrls, videoTitle, context.Url).ToArray();

                try {
                    var htmlPlayerVersion = GetHtml5PlayerVersion(json);


                    foreach (var info in infos) {
                        info.HtmlPlayerVersion = htmlPlayerVersion;

                        if (decryptSignature && info.RequiresDecryption)
                            DecryptDownloadUrl(info);
                    }

                    return infos;
                } catch (Exception e) {
                    rpf.Defaultize(e);
                    context.OnDownloadFailed(rpf);
                    Console.WriteLine(e);
                    if (rpf.ShouldRetry)
                        goto _redownload;
                    return null;
                }
            } catch (Exception ex) when (ex.Message == "Result cannot be called on a failed Match.") {
                goto _retry;
            } catch (Exception ex) {
                if (ex is WebException || ex is VideoNotAvailableException)
                    throw;

                ThrowYoutubeParseException(ex, context.Url);
            } //Message 

            return null; // Will never happen, but the compiler requires it
        }
        /// <summary>
        ///     Extracts the playlist from the url, wether its on a playlist page or a side playlist when playing a video.
        ///     Will return the urls of the songs
        /// </summary>
        public static async Task<List<string>> ExtractPlaylistAsync(string url) {
            url = NormalizeYoutubePlaylistUrl(url);
            if (string.IsNullOrEmpty(url))
                throw new ArgumentNullException(nameof(url));

            string pageSource;
            var rpf = new RetryableProcessFailed("LoadUrls") {Tag = url};
            retry:
            try {
                pageSource = await _httpClient.DownloadStringTaskAsync(url);
            } catch (Exception e) {
                rpf.Defaultize(e);
                //TODO FailedDownload?.Invoke(rpf);
                if (rpf.ShouldRetry)
                    goto retry;
                return null;
            }

            var doc = new HtmlDocument();
            doc.LoadHtml(pageSource);
            return _extractPlaylistUrls(url, doc);
        }
        private static async Task<JObject> LoadJsonAsync(string url) {
            string pageSource;
            var rpf = new RetryableProcessFailed("LoadUrls") {Tag = url};
            retry:
            try {
                pageSource = await _httpClient.DownloadStringTaskAsync(url);
            } catch (Exception e) {
                rpf.Defaultize(e);
                //TODO FailedDownload?.Invoke(rpf);
                if (rpf.ShouldRetry)
                    goto retry;
                return null;
            }

            if (IsVideoUnavailable(pageSource))
                throw new VideoNotAvailableException();

            var dataRegex = new Regex(@"ytplayer\.config\s*=\s*(\{.+?\});", RegexOptions.Multiline);

            var extractedJson = dataRegex.Match(pageSource).Result("$1");

            return JObject.Parse(extractedJson);
        }
        private static JObject LoadJson(string url) {
            string pageSource;
            var rpf = new RetryableProcessFailed("LoadUrls") {Tag = url};
            var timeout = 1500u;
            retry:
            try {
                pageSource = HttpHelper.DownloadString(url, timeout);
            } catch (Exception e) {
                rpf.Defaultize(e);
                //TODO FailedDownload?.Invoke(rpf);
                
                if (rpf.ShouldRetry) {
                    timeout += 500;
                    goto retry;
                }
                return null;
            }

            if (IsVideoUnavailable(pageSource))
                throw new VideoNotAvailableException();

            var dataRegex = new Regex(@"ytplayer\.config\s*=\s*(\{.+?\});", RegexOptions.Multiline);

            var extractedJson = dataRegex.Match(pageSource).Result("$1");

            return JObject.Parse(extractedJson);
        }
        public async Task ExecuteAsync() {
            context.OnProgresStateChanged(YoutubeStage.StartingDownload);
            if (context.VideoInfo == null) throw new InvalidOperationException("Cant extract audio when no VideoInfo is selected.");

            HttpResponseMessage response;
            var rpf = new RetryableProcessFailed("Video Downloader") {Tag=context};
            retry:try {
                response = await _httpClient.GetAsync(context.VideoInfo.DownloadUrl);

            } catch (Exception e) {
                rpf.Defaultize(e);
                context.OnDownloadFailed(rpf);
                if (rpf.ShouldRetry)
                    goto retry;
                return;
            }

            if (!response.IsSuccessStatusCode)
                throw new Exception();
            context.VideoPath = new FileInfo(context.VideoPath?.FullName ?? context.VideoSaveableFilename);

            using (var downloadStream = await response.Content.ReadAsStreamAsync())
            using (var fileStream = File.Open(context.VideoPath.FullName, FileMode.Create, FileAccess.Write)) {
                var buffer = new byte[0x4000]; //16KB buffer
                var cancelRequest = false;

                int bytes;
                double bytesDownloaded = 0;

                while (!cancelRequest && (bytes = await downloadStream.ReadAsync(buffer, 0, buffer.Length)) > 0) {
                    await fileStream.WriteAsync(buffer, 0, bytes);
                    bytesDownloaded += bytes;

                    var e = context.OnProgresStateChanged(YoutubeStage.Downloading, ((bytesDownloaded / downloadStream.Length) * 100));

                    if (e.Cancel)
                        cancelRequest = true;
                }
            }
            context.OnProgresStateChanged(YoutubeStage.DownloadFinished);
        }
        /// <summary>
        ///     Gets a list of <see cref="VideoInfo" />s for the specified URL.
        /// </summary>
        /// <param name="context">The context, must contain a url to the video in the Url property</param>
        /// <param name="decryptSignature">
        ///     A value indicating whether the video signatures should be decrypted or not. Decrypting
        ///     consists of a HTTP request for each <see cref="VideoInfo" />, so you may want to set
        ///     this to false and call <see cref="DecryptDownloadUrl" /> on your selected
        ///     <see
        ///         cref="VideoInfo" />
        ///     later.
        /// </param>
        /// <returns>A list of <see cref="VideoInfo" />s that can be used to download the video.</returns>
        /// <exception cref="VideoNotAvailableException">The video is not available.</exception>
        /// <exception cref="WebException">
        ///     An error occurred while downloading the YouTube page html.
        /// </exception>
        /// <exception cref="YoutubeParseException">The Youtube page could not be parsed.</exception>
        public static async Task<IEnumerable<VideoInfo>> GetDownloadUrlsAsync(YoutubeContext context, bool decryptSignature = true) {
            if (context == null)
                throw new ArgumentNullException(nameof(context));
            if (context.Url == null)
                throw new ArgumentNullException(nameof(context.Url));

            context.OnProgresStateChanged(YoutubeStage.ProcessingUrls);
            string ytb;
            var isYoutubeUrl = TryNormalizeYoutubeUrl(context.Url, out ytb);
            context.Url = ytb;

            if (!isYoutubeUrl)
                throw new ArgumentException("URL is not a valid youtube URL!");

            try {
                var rpf = new RetryableProcessFailed("ParseHtml5Version") {Tag = context.Url };
                _redownload:
                var json = await LoadJsonAsync(context.Url);
                var videoTitle = GetVideoTitle(json);
                int n = 0;
                var downloadUrls = ExtractDownloadUrls(json);
                var infos = GetVideoInfos(downloadUrls, videoTitle, context.Url);
                string htmlPlayerVersion;

                try {
                    htmlPlayerVersion = GetHtml5PlayerVersion(json);
                } catch (Exception e) {
                    rpf.Defaultize(e);
                    context.OnDownloadFailed(rpf);
                    if (rpf.ShouldRetry)
                        goto _redownload;
                    return null;
                }

                foreach (var info in infos) {
                    info.HtmlPlayerVersion = htmlPlayerVersion;

                    if (decryptSignature && info.RequiresDecryption)
                        await DecryptDownloadUrlAsync(info);
                }

                return infos;
            } catch (Exception ex) {
                if (ex is WebException || ex is VideoNotAvailableException)
                    throw;

                ThrowYoutubeParseException(ex, context.Url);
            }

            return null; // Will never happen, but the compiler requires it
        }
        public static async Task<string> DecipherWithVersionAsync(VideoInfo vidinfo, string cipher, string cipherVersion) {
            var jsUrl = $"http://s.ytimg.com/yts/jsbin/player-{cipherVersion}.js";

            string js;
            var rpf = new RetryableProcessFailed("LoadUrls") {Tag = vidinfo};

            var timeout = 1500u;
            retry:
            try {
                js = await HttpHelper.DownloadStringAsync(jsUrl, timeout);
            } catch (Exception e) {
                rpf.Defaultize(e);
                if (rpf.ShouldRetry && rpf.NumberOfTries <= 10) {
                    timeout += 500;
                    goto retry;
                }
                return null;
            }


            //Find "C" in this: var A = B.sig||C (B.s)
            var functNamePattern = @"\.sig\s*\|\|([a-zA-Z0-9\$]+)\("; //Regex Formed To Find Word or DollarSign
            var funcName = Regex.Match(js, functNamePattern).Groups[1].Value;

            if (funcName.Contains("$"))
                funcName = "\\" + funcName; //Due To Dollar Sign Introduction, Need To Escape

            var funcBodyPattern = @"(?<brace>{([^{}]| ?(brace))*})"; //Match nested angle braces
            var funcPattern = @"var " + @funcName + @"=function\(\w+\)\{.*?\};"; //Escape funcName string
            var funcBody = Regex.Match(js, funcPattern).Value; //Entire sig function
            var lines = funcBody.Split(';'); //Each line in sig function

            string idReverse = "", idSlice = "", idCharSwap = ""; //Hold name for each cipher method
            var functionIdentifier = "";
            var operations = "";

            foreach (var line in lines.Skip(1).Take(lines.Length - 2)) //Matches the funcBody with each cipher method. Only runs till all three are defined.
            {
                if (!string.IsNullOrEmpty(idReverse) && !string.IsNullOrEmpty(idSlice) &&
                    !string.IsNullOrEmpty(idCharSwap))
                    break; //Break loop if all three cipher methods are defined

                functionIdentifier = GetFunctionFromLine(line);
                var reReverse = $@"{functionIdentifier}:\bfunction\b\(\w+\)"; //Regex for reverse (one parameter)
                var reSlice = $@"{functionIdentifier}:\bfunction\b\([a],b\).(\breturn\b)?.?\w+\."; //Regex for slice (return or not)
                var reSwap = $@"{functionIdentifier}:\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b"; //Regex for the char swap.

                if (Regex.Match(js, reReverse).Success)
                    idReverse = functionIdentifier; //If def matched the regex for reverse then the current function is a defined as the reverse

                if (Regex.Match(js, reSlice).Success)
                    idSlice = functionIdentifier; //If def matched the regex for slice then the current function is defined as the slice.

                if (Regex.Match(js, reSwap).Success)
                    idCharSwap = functionIdentifier; //If def matched the regex for charSwap then the current function is defined as swap.
            }

            foreach (var line in lines.Skip(1).Take(lines.Length - 2)) {
                Match m;
                functionIdentifier = GetFunctionFromLine(line);

                if ((m = Regex.Match(line, @"\(\w+,(?<index>\d+)\)")).Success && functionIdentifier == idCharSwap)
                    operations += "w" + m.Groups["index"].Value + " "; //operation is a swap (w)

                if ((m = Regex.Match(line, @"\(\w+,(?<index>\d+)\)")).Success && functionIdentifier == idSlice)
                    operations += "s" + m.Groups["index"].Value + " "; //operation is a slice

                if (functionIdentifier == idReverse) //No regex required for reverse (reverse method has no parameters)
                    operations += "r "; //operation is a reverse
            }

            operations = operations.Trim();

            return DecipherWithOperations(cipher, operations);
        }