using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Google.Apis.YouTube.v3.Data;
using Microsoft.AppCenter.Analytics;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using Windows.ApplicationModel.Resources;
using Windows.Storage;
using YoutubeExplode.Models.MediaStreams;

namespace FoxTube.Controls.Player
{
    public static class ManifestGenerator
    {
        static readonly StorageFolder roaming = ApplicationData.Current.RoamingFolder;
        public static async Task<Uri> GetManifest(Video meta, VideoStreamInfo requestedQuality, MediaStreamInfoSet list)
        {
            StorageFile manifest;
            try { manifest = await roaming.CreateFileAsync("manifest.mpd", CreationCollisionOption.ReplaceExisting); }
            catch { manifest = await roaming.CreateFileAsync("manifest.mpd", CreationCollisionOption.GenerateUniqueName); }

            try
            {
                XmlDocument doc = new XmlDocument();

                XmlElement mpd = doc.CreateElement("MPD");
                mpd.SetAttribute("mediaPresentationDuration", meta.ContentDetails.Duration);
                mpd.SetAttribute("minBufferTime", "PT2S");
                mpd.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011");
                mpd.SetAttribute("type", "static");

                XmlElement period = doc.CreateElement("Period");

                XmlElement videoSet = doc.CreateElement("AdaptationSet");
                XmlElement videoMeta = doc.CreateElement("ContentComponent");
                videoMeta.SetAttribute("contentType", "video");
                videoMeta.SetAttribute("id", "1");
                videoSet.AppendChild(videoMeta);

                StreamInfo streamInfo = await GetInfoAsync(meta, requestedQuality);

                foreach (var i in streamInfo.Video)
                    videoSet.AppendChild(GetVideoPresentation(doc, i));

                XmlElement audioSet = doc.CreateElement("AdaptationSet");
                XmlElement audioMeta = doc.CreateElement("ContentComponent");
                audioMeta.SetAttribute("contentType", "audio");
                audioMeta.SetAttribute("id", "2");
                audioSet.AppendChild(audioMeta);

                foreach (var i in streamInfo.Audio)
                    audioSet.AppendChild(GetAudioPresentation(doc, i));

                doc.AppendChild(mpd);
                mpd.AppendChild(period);
                period.AppendChild(videoSet);
                period.AppendChild(audioSet);

                doc.Save(await manifest.OpenStreamForWriteAsync());

                return $"ms-appdata:///roaming/{manifest.Name}".ToUri();
            }
            catch (Exception e)
            {
                Analytics.TrackEvent("Failed to generate manifest", new Dictionary<string, string>
                {
                    { "Exception", e.GetType().ToString() },
                    { "Message", e.Message },
                    { "Video ID", meta.Id },
                    { "Requested quality", requestedQuality.VideoQualityLabel },
                    { "StackTrace", e.StackTrace }
                });
                return null;
            }
        }

        private static XmlElement GetVideoPresentation(XmlDocument doc, StreamInfo.VideoInfo info)
        {
            XmlElement representation = doc.CreateElement("Representation");
            representation.SetAttribute("bandwidth", GetBandwidth(info.Label));
            representation.SetAttribute("id", info.Itag);
            representation.SetAttribute("mimeType", info.MimeType);
            representation.SetAttribute("codecs", info.Codecs);
            representation.SetAttribute("fps", info.Fps);
            representation.SetAttribute("height", info.Height);
            representation.SetAttribute("width", info.Width);

            XmlElement baseUrl = doc.CreateElement("BaseURL");
            baseUrl.InnerText = info.Url;
            representation.AppendChild(baseUrl);

            XmlElement segmentBase = doc.CreateElement("SegmentBase");
            segmentBase.SetAttribute("indexRange", info.IndexRange);
            representation.AppendChild(segmentBase);

            XmlElement initialization = doc.CreateElement("Initialization");
            initialization.SetAttribute("range", info.InitRange);
            segmentBase.AppendChild(initialization);

            return representation;
        }

        private static XmlElement GetAudioPresentation(XmlDocument doc, StreamInfo.AudioInfo info)
        {
            XmlElement audio = doc.CreateElement("Representation");
            audio.SetAttribute("bandwidth", "200000");
            audio.SetAttribute("id", info.Itag);
            audio.SetAttribute("sampleRate", info.SampleRate);
            audio.SetAttribute("numChannels", info.ChannelsCount);
            audio.SetAttribute("codecs", info.Codecs);
            audio.SetAttribute("mimeType", info.MimeType);

            XmlElement audioUrl = doc.CreateElement("BaseURL");
            audioUrl.InnerText = info.Url;
            audio.AppendChild(audioUrl);

            XmlElement audioSegmentBase = doc.CreateElement("SegmentBase");
            audioSegmentBase.SetAttribute("indexRange", info.IndexRange);
            audio.AppendChild(audioSegmentBase);

            XmlElement audioInit = doc.CreateElement("Initialization");
            audioInit.SetAttribute("range", info.InitRange);
            audioSegmentBase.AppendChild(audioInit);

            return audio;
        }

        private static async Task<StreamInfo> GetInfoAsync(Video info, VideoStreamInfo requestedQuality)
        {
            try
            {
                StreamInfo si = new StreamInfo();
                HttpClient http = new HttpClient();

                string response = await http.GetStringAsync($"https://youtube.com/watch?v={info.Id}&disable_polymer=true&bpctr=9999999999&hl=en");
                IHtmlDocument videoEmbedPageHtml = new HtmlParser().ParseDocument(response);

                string playerConfigRaw = Regex.Match(videoEmbedPageHtml.Source.Text,
                    @"ytplayer\.config = (?<Json>\{[^\{\}]*(((?<Open>\{)[^\{\}]*)+((?<Close-Open>\})[^\{\}]*)+)*(?(Open)(?!))\})")
                    .Groups["Json"].Value;
                JToken playerConfigJson = JToken.Parse(playerConfigRaw);

                var playerResponseRaw = playerConfigJson.SelectToken("args.player_response").Value<string>();
                JToken playerResponseJson = JToken.Parse(playerResponseRaw);
                string errorReason = playerResponseJson.SelectToken("playabilityStatus.reason")?.Value<string>();
                if (!string.IsNullOrWhiteSpace(errorReason))
                    throw new InvalidDataException($"Video [{info.Id}] is unplayable. Reason: {errorReason}");

                List<Dictionary<string, string>>  adaptiveStreamInfosUrl = playerConfigJson.SelectToken("args.adaptive_fmts")?.Value<string>().Split(',').Select(SplitQuery).ToList();
                List<Dictionary<string, string>> video = 
                    requestedQuality == null ?
                        adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("quality_label")) :
                        adaptiveStreamInfosUrl.FindAll(i => i.ContainsValue(requestedQuality.VideoQualityLabel));
                List<Dictionary<string, string>>  audio = adaptiveStreamInfosUrl.FindAll(i => i.ContainsKey("audio_sample_rate"));

                foreach (var i in video)
                    si.Video.Add(new StreamInfo.VideoInfo
                    {
                        IndexRange = i["index"],
                        Url = i["url"],
                        Itag = i["itag"],
                        Fps = i["fps"],
                        Height = i["size"].Split('x')[1],
                        Width = i["size"].Split('x')[0],
                        Codecs = i["type"].Split('"')[1],
                        MimeType = i["type"].Split(';')[0],
                        Label = i["quality_label"]
                    });

                foreach (var i in audio)
                    si.Audio.Add(new StreamInfo.AudioInfo
                    {
                        ChannelsCount = i["audio_channels"],
                        IndexRange = i["index"],
                        SampleRate = i["audio_sample_rate"],
                        Codecs = i["type"].Split('"')[1],
                        MimeType = i["type"].Split(';')[0],
                        Url = i["url"],
                        Itag = i["itag"]
                    });

                return si;
            }
            catch
            {
                return new StreamInfo();
            }
        }

        public static Dictionary<string, string> SplitQuery(string query)
        {
            Dictionary<string, string> dic = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            string[] paramsEncoded = query.TrimStart('?').Split("&");
            foreach (string paramEncoded in paramsEncoded)
            {
                string param = WebUtility.UrlDecode(paramEncoded);

                // Look for the equals sign
                int equalsPos = param.IndexOf('=');
                if (equalsPos <= 0)
                    continue;

                // Get the key and value
                string key = param.Substring(0, equalsPos);
                string value = equalsPos < param.Length
                    ? param.Substring(equalsPos + 1)
                    : string.Empty;

                // Add to dictionary
                dic[key] = value;
            }

            return dic;
        }

        private static string GetBandwidth(string quality)
        {
            string parsed = quality.Split('p')[0];
            switch (quality)
            {
                case "4320":
                    return $"16763040‬";
                case "3072":
                    return $"11920384";
                case "2880":
                    return $"11175360";
                case "2160":
                    return $"8381520";
                case "1440":
                    return $"5587680‬";
                case "1080":
                    return $"4190760";
                case "720":
                    return $"2073921";
                case "480":
                    return $"869460";
                case "360":
                    return $"686521";
                case "240":
                    return $"264835";
                case "144":
                default:
                    return $"100000";
            }
        }

        public static async Task<List<StreamQuality>> ResolveLiveSteream(string url)
        {
            List<StreamQuality> list = new List<StreamQuality>();
            string playlistRaw = await new HttpClient().GetStringAsync(url);

            List<string> streamsRaw = playlistRaw.Split("#EXT-X-STREAM-INF:").ToList();
            streamsRaw.RemoveAt(0);
            List<Dictionary<string, string>> streams = new List<Dictionary<string, string>>();
            foreach (string i in streamsRaw)
            {
                Dictionary<string, string> item = new Dictionary<string, string>();
                string[] par = i.Split('\n');
                item.Add("URL", par[1]);
                par = par[0].Split(',');
                foreach (string k in par)
                {
                    string[] pair = k.Split('=');
                    if (pair.Length < 2)
                        continue;
                    item[pair[0]] = pair[1];
                }
                streams.Add(item);
            }

            foreach (var i in streams)
            {
                StreamQuality item = new StreamQuality();
                item.Resolution = $"{i["RESOLUTION"].Split('x')[1]}p";
                item.Url = i["URL"].ToUri();
                list.Add(item);
            }

            list.Add(new StreamQuality
            {
                Resolution = ResourceLoader.GetForCurrentView("VideoPage").GetString("/VideoPage/auto"),
                Url = url.ToUri()
            });
            list.Reverse();

            return list;
        }

        public static async void ClearRoaming()
        {
            IReadOnlyList<StorageFile> items = await roaming.GetFilesAsync();
            foreach (StorageFile f in items)
                await f.DeleteAsync(StorageDeleteOption.PermanentDelete);
        }
    }

    public class StreamInfo
    {
        public class VideoInfo
        {
            public string IndexRange { get; set; }
            public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}";
            public string Itag { get; set; }
            public string Fps { get; set; }
            public string Url { get; set; }
            public string Codecs { get; set; }
            public string MimeType { get; set; }
            public string Height { get; set; }
            public string Width { get; set; }
            public string Label { get; set; }
        }
        public class AudioInfo
        {
            public string IndexRange { get; set; }
            public string InitRange => $"0-{int.Parse(IndexRange.Split('-')[0]) - 1}";
            public string SampleRate { get; set; }
            public string ChannelsCount { get; set; }
            public string Codecs { get; set; }
            public string MimeType { get; set; }
            public string Url { get; set; }
            public string Itag { get; set; }
        }

        public List<VideoInfo> Video { get; } = new List<VideoInfo>();
        public List<AudioInfo> Audio { get; } = new List<AudioInfo>();
    }

    public class StreamQuality
    {
        public Uri Url { get; set; }
        public string Resolution { get; set; }
    }
}
