using LibAudioVisualizer;
using NAudio.CoreAudioApi;
using NAudio.Wave;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace WpfAudioVisualizer
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        WasapiCapture capture;             // 音频捕获
        Visualizer visualizer;             // 可视化
        Timer? dataTimer;
        Timer? drawingTimer;

        double[]? spectrumData;            // 频谱数据

        Color[] allColors;                 // 渐变颜色

        public MainWindow()
        {
            capture = new WasapiLoopbackCapture();          // 捕获电脑发出的声音
            visualizer = new Visualizer(256);               // 新建一个可视化器, 并使用 256 个采样进行傅里叶变换

            allColors = GetAllHsvColors();                  // 获取所有的渐变颜色 (HSV 颜色)

            capture.WaveFormat = WaveFormat.CreateIeeeFloatWaveFormat(8192, 1);      // 指定捕获的格式, 单声道, 32位深度, IeeeFloat 编码, 8192采样率
            capture.DataAvailable += Capture_DataAvailable;                          // 订阅事件

            InitializeComponent();
        }
        /// <summary>
        /// 简单的数据模糊
        /// </summary>
        /// <param name="data">数据</param>
        /// <param name="radius">模糊半径</param>
        /// <returns>结果</returns>
        private double[] MakeSmooth(double[] data, int radius)
        {
            double[] GetWeights(int radius)
            {
                double Gaussian(double x) => Math.Pow(Math.E, (-4 * x * x));        // 憨批高斯函数

                int len = 1 + radius * 2;                         // 长度
                int end = len - 1;                                // 最后的索引
                double radiusF = (double)radius;                    // 半径浮点数
                double[] weights = new double[len];                 // 权重

                for (int i = 0; i <= radius; i++)                 // 先把右边的权重算出来
                    weights[radius + i] = Gaussian(i / radiusF);
                for (int i = 0; i < radius; i++)                  // 把右边的权重拷贝到左边
                    weights[i] = weights[end - i];

                double total = weights.Sum();
                for (int i = 0; i < len; i++)                  // 使权重合为 0
                    weights[i] = weights[i] / total;

                return weights;
            }

            void ApplyWeights(double[] buffer, double[] weights)
            {
                int len = buffer.Length;
                for (int i = 0; i < len; i++)
                    buffer[i] = buffer[i] * weights[i];
            }


            double[] weights = GetWeights(radius);
            double[] buffer = new double[1 + radius * 2];

            double[] result = new double[data.Length];
            if (data.Length < radius)
            {
                Array.Fill(result, data.Average());
                return result;
            }


            for (int i = 0; i < radius; i++)
            {
                Array.Fill(buffer, data[i], 0, radius + 1);      // 填充缺省
                for (int j = 0; j < radius; j++)                 // 
                {
                    buffer[radius + 1 + j] = data[i + j];
                }

                ApplyWeights(buffer, weights);
                result[i] = buffer.Sum();
            }

            for (int i = radius; i < data.Length - radius; i++)
            {
                for (int j = 0; j < radius; j++)                 // 
                {
                    buffer[j] = data[i - j];
                }

                buffer[radius] = data[i];

                for (int j = 0; j < radius; j++)                 // 
                {
                    buffer[radius + j + 1] = data[i + j];
                }

                ApplyWeights(buffer, weights);
                result[i] = buffer.Sum();
            }

            for (int i = data.Length - radius; i < data.Length; i++)
            {
                Array.Fill(buffer, data[i], 0, radius + 1);      // 填充缺省
                for (int j = 0; j < radius; j++)                 // 
                {
                    buffer[radius + 1 + j] = data[i - j];
                }

                ApplyWeights(buffer, weights);
                result[i] = buffer.Sum();
            }

            return result;
        }

        /// <summary>
        /// 获取 HSV 中所有的基础颜色 (饱和度和明度均为最大值)
        /// </summary>
        /// <returns>所有的 HSV 基础颜色(共 256 * 6 个, 并且随着索引增加, 颜色也会渐变)</returns>
        private Color[] GetAllHsvColors()
        {
            Color[] result = new Color[256 * 6];

            for (int i = 0; i <= 255; i++)
            {
                result[i] = Color.FromArgb(255, 255, (byte)i, 0);
            }

            for (int i = 0; i <= 255; i++)
            {
                result[256 + i] = Color.FromArgb(255, (byte)(255 - i), 255, 0);
            }

            for (int i = 0; i <= 255; i++)
            {
                result[512 + i] = Color.FromArgb(255, 0, 255, (byte)i);
            }

            for (int i = 0; i <= 255; i++)
            {
                result[768 + i] = Color.FromArgb(255, 0, (byte)(255 - i), 255);
            }

            for (int i = 0; i <= 255; i++)
            {
                result[1024 + i] = Color.FromArgb(255, (byte)i, 0, 255);
            }

            for (int i = 0; i <= 255; i++)
            {
                result[1280 + i] = Color.FromArgb(255, 255, 0, (byte)(255 - i));
            }

            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Capture_DataAvailable(object? sender, WaveInEventArgs e)
        {
            int length = e.BytesRecorded / 4;           // 采样的数量 (每一个采样是 4 字节)
            double[] result = new double[length];       // 声明结果

            for (int i = 0; i < length; i++)
                result[i] = BitConverter.ToSingle(e.Buffer, i * 4);      // 取出采样值

            visualizer.PushSampleData(result);          // 将新的采样存储到 可视化器 中
        }

        /// <summary>
        /// 用来刷新频谱数据以及实现频谱数据缓动
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DataTimer_Tick(object? state)
        {
            double[] newSpectrumData = visualizer.GetSpectrumData("");         // 从可视化器中获取频谱数据
            newSpectrumData = MakeSmooth(newSpectrumData, 2);                // 平滑频谱数据

            if (spectrumData == null)                                        // 如果已经存储的频谱数据为空, 则把新的频谱数据直接赋值上去
            {
                spectrumData = newSpectrumData;
                return;
            }

            for (int i = 0; i < newSpectrumData.Length; i++)                 // 计算旧频谱数据和新频谱数据之间的 "中间值"
            {
                double oldData = spectrumData[i];
                double newData = newSpectrumData[i];
                double lerpData = oldData + (newData - oldData) * .2f;            // 每一次执行, 频谱值会向目标值移动 20% (如果太大, 缓动效果不明显, 如果太小, 频谱会有延迟的感觉)
                spectrumData[i] = lerpData;
            }
        }

        /// <summary>
        /// 绘制一个渐变的 波浪
        /// </summary>
        /// <param name="g">绘图目标</param>
        /// <param name="down">下方颜色</param>
        /// <param name="up">上方颜色</param>
        /// <param name="spectrumData">频谱数据</param>
        /// <param name="pointCount">波浪中, 点的数量</param>
        /// <param name="drawingWidth">波浪的宽度</param>
        /// <param name="xOffset">波浪的起始X坐标</param>
        /// <param name="yOffset">波浪的其实Y坐标</param>
        /// <param name="scale">频谱的缩放(使用负值可以翻转波浪)</param>
        private void DrawGradient(Path g, Color down, Color up, double[] spectrumData, int pointCount, double drawingWidth, double xOffset, double yOffset, double scale)
        {
            Point[] points = new Point[pointCount + 2];
            for (int i = 0; i < pointCount; i++)
            {
                double x = i * drawingWidth / pointCount + xOffset;
                double y = spectrumData[i * spectrumData.Length / pointCount] * scale + yOffset;
                points[i + 1] = new Point(x, y);
            }

            points[0] = new Point(xOffset, yOffset);
            points[points.Length - 1] = new Point(xOffset + drawingWidth, yOffset);

            double upP = points.Min(v => v.Y);

            if (Math.Abs(upP - yOffset) < 1)
                return;

            g.Data = new PathGeometry() { Figures = { new PathFigure() { IsFilled = true, Segments = { new PolyLineSegment(points, false) } } } };
            g.Fill = new LinearGradientBrush(down, up, new Point(0, yOffset), new Point(0, upP));
        }

        /// <summary>
        /// 绘制渐变的条形
        /// </summary>
        /// <param name="g">绘图目标</param>
        /// <param name="down">下方颜色</param>
        /// <param name="up">上方颜色</param>
        /// <param name="spectrumData">频谱数据</param>
        /// <param name="stripCount">条形的数量</param>
        /// <param name="drawingWidth">绘图的宽度</param>
        /// <param name="xOffset">绘图的起始 X 坐标</param>
        /// <param name="yOffset">绘图的起始 Y 坐标</param>
        /// <param name="spacing">条形与条形之间的间隔(像素)</param>
        /// <param name="scale"></param>
        private void DrawGradientStrips(Path g, Color down, Color up, double[] spectrumData, int stripCount, double drawingWidth, double xOffset, double yOffset, double spacing, double scale)
        {
            double stripWidth = (drawingWidth - spacing * stripCount) / stripCount;
            Point[] points = new Point[stripCount];

            for (int i = 0; i < stripCount; i++)
            {
                double x = stripWidth * i + spacing * i + xOffset;
                double y = spectrumData[i * spectrumData.Length / stripCount] * scale;   // height
                points[i] = new Point(x, y);
            }

            double upP = points.Min(v => v.Y < 0 ? yOffset + v.Y : yOffset);
            double downP = points.Max(v => v.Y < 0 ? yOffset : yOffset + v.Y);

            if (downP < yOffset)
                downP = yOffset;

            GeometryGroup geo = new GeometryGroup();
            Brush brush = new LinearGradientBrush(down, up, new Point(0, downP), new Point(0, upP));

            for (int i = 0; i < stripCount; i++)
            {
                Point p = points[i];
                double y = yOffset;
                double height = p.Y;

                if (height < 0)
                {
                    y += height;
                    height = -height;
                }

                Point[] endPoints = new Point[]
                {
                    new Point(p.X, p.Y),
                    new Point(p.X, p.Y + height),
                    new Point(p.X + stripWidth, p.Y + height),
                    new Point(p.X + stripWidth, p.Y)
                };

                PathFigure fig = new PathFigure();

                fig.StartPoint = endPoints[0];
                fig.Segments.Add(new PolyLineSegment(endPoints, false));
                //fig.IsClosed = true;

                geo.Children.Add(new PathGeometry() { Figures = { fig } });
            }

            g.Data = geo;
            g.Fill = brush;
        }

        /// <summary>
        /// 画曲线
        /// </summary>
        /// <param name="g"></param>
        /// <param name="brush"></param>
        /// <param name="spectrumData"></param>
        /// <param name="pointCount"></param>
        /// <param name="drawingWidth"></param>
        /// <param name="xOffset"></param>
        /// <param name="yOffset"></param>
        /// <param name="scale"></param>
        private void DrawCurve(Path g, Brush brush, double[] spectrumData, int pointCount, double drawingWidth, double xOffset, double yOffset, double scale)
        {
            Point[] points = new Point[pointCount];
            for (int i = 0; i < pointCount; i++)
            {
                double x = i * drawingWidth / pointCount + xOffset;
                double y = spectrumData[i * spectrumData.Length / pointCount] * scale + yOffset;
                points[i] = new Point(x, y);
            }

            PathFigure fig = new PathFigure();
            fig.Segments.Add(new PolyLineSegment(points, true));

            g.Data = new PathGeometry() { Figures = { fig } };
            g.Stroke = brush;
        }

        private void DrawCircleStrips(Path g, Brush brush, double[] spectrumData, int stripCount, double xOffset, double yOffset, double radius, double spacing, double rotation, double scale)
        {
            double rotationAngle = Math.PI / 180 * rotation;
            double blockWidth = MathF.PI * 2 / stripCount;           // angle
            double stripWidth = blockWidth - MathF.PI / 180 * spacing;                // angle
            Point[] points = new Point[stripCount];

            for (int i = 0; i < stripCount; i++)
            {
                double x = blockWidth * i + rotationAngle;      // angle
                double y = spectrumData[i * spectrumData.Length / stripCount] * scale;   // height
                points[i] = new Point(x, y);
            }

            PathGeometry geo = new PathGeometry();

            for (int i = 0; i < stripCount; i++)
            {
                Point p = points[i];
                double sinStart = Math.Sin(p.X);
                double sinEnd = Math.Sin(p.X + stripWidth);
                double cosStart = Math.Cos(p.X);
                double cosEnd = Math.Cos(p.X + stripWidth);

                Point[] polygon = new Point[]
                {
                    new Point((cosStart * radius + xOffset), (sinStart * radius + yOffset)),
                    new Point((cosEnd * radius + xOffset), (sinEnd * radius + yOffset)),
                    new Point((cosEnd * (radius + p.Y) + xOffset), (sinEnd * (radius + p.Y) + yOffset)),
                    new Point((cosStart * (radius + p.Y) + xOffset), (sinStart * (radius + p.Y) + yOffset)),
                };

                PathFigure fig = new PathFigure();

                fig.IsFilled = true;
                fig.Segments.Add(new PolyLineSegment(polygon, false));

                geo.Figures.Add(fig);
            }

            g.Data = geo;
            g.Fill = brush;
        }

        /// <summary>
        /// 画圆环条
        /// </summary>
        /// <param name="g"></param>
        /// <param name="inner"></param>
        /// <param name="outer"></param>
        /// <param name="spectrumData"></param>
        /// <param name="stripCount"></param>
        /// <param name="xOffset"></param>
        /// <param name="yOffset"></param>
        /// <param name="radius"></param>
        /// <param name="spacing"></param>
        /// <param name="scale"></param>
        private void DrawCircleGradientStrips(Path g, Color inner, Color outer, double[] spectrumData, int stripCount, double xOffset, double yOffset, double radius, double spacing, double rotation, double scale)
        {
            double rotationAngle = Math.PI / 180 * rotation;
            double blockWidth = Math.PI * 2 / stripCount;           // angle
            double stripWidth = blockWidth - MathF.PI / 180 * spacing;                // angle
            Point[] points = new Point[stripCount];

            for (int i = 0; i < stripCount; i++)
            {
                double x = blockWidth * i + rotationAngle;      // angle
                double y = spectrumData[i * spectrumData.Length / stripCount] * scale;   // height
                points[i] = new Point(x, y);
            }

            double maxHeight = points.Max(v =>  v.Y);
            double outerRadius = radius + maxHeight;

            PathGeometry geo = new PathGeometry();

            for (int i = 0; i < stripCount; i++)
            {
                Point p = points[i];
                double sinStart = Math.Sin(p.X);
                double sinEnd = Math.Sin(p.X + stripWidth);
                double cosStart = Math.Cos(p.X);
                double cosEnd = Math.Cos(p.X + stripWidth);

                Point[] polygon = new Point[]
                {
                    new Point((cosStart * radius + xOffset),(sinStart * radius + yOffset)),
                    new Point((cosEnd * radius + xOffset),(sinEnd * radius + yOffset)),
                    new Point((cosEnd * (radius + p.Y) + xOffset), (sinEnd * (radius + p.Y) + yOffset)),
                    new Point((cosStart * (radius + p.Y) + xOffset), (sinStart * (radius + p.Y) + yOffset))
                };

                PathFigure fig = new PathFigure();

                fig.IsFilled = true;
                fig.Segments.Add(new PolyLineSegment(polygon, false));

                geo.Figures.Add(fig);
            }

            LinearGradientBrush brush = new LinearGradientBrush(
                new GradientStopCollection() { new GradientStop(Colors.Transparent, 0), new GradientStop(inner, radius / (radius + maxHeight)), new GradientStop(outer, 1) },
                new Point(xOffset, yOffset),
                new Point(xOffset, yOffset + radius + maxHeight));
            g.Data = geo;
            g.Fill = brush;
        }

        private void DrawStrips(Path g, Brush brush, double[] spectrumData, int stripCount, int drawingWidth, float xOffset, float yOffset, float spacing, double scale)
        {
            float stripWidth = (drawingWidth - spacing * stripCount) / stripCount;
            Point[] points = new Point[stripCount];

            for (int i = 0; i < stripCount; i++)
            {
                double x = stripWidth * i + spacing * i + xOffset;
                double y = spectrumData[i * spectrumData.Length / stripCount] * scale;   // height
                points[i] = new Point(x, y);
            }

            PathGeometry geo = new PathGeometry();

            for (int i = 0; i < stripCount; i++)
            {
                Point p = points[i];
                double y = yOffset;
                double height = p.Y;

                if (height < 0)
                {
                    y += height;
                    height = -height;
                }

                Point[] endPoints = new Point[]
                {
                    new Point(p.X, y),
                    new Point(p.X, y + height),
                    new Point(p.X + stripWidth, y + height),
                    new Point(p.X + stripWidth, y)
                };

                PathFigure fig = new PathFigure();

                fig.IsFilled = true;
                fig.Segments.Add(new PolyLineSegment(endPoints, false));

                geo.Figures.Add(fig);
            }

            g.Data = geo;
            g.Fill = brush;
        }

        private void DrawGradientBorder(
            Rectangle upBorder, Rectangle downBorder, Rectangle leftBorder, Rectangle rightBorder,
            Color inner, Color outer, double scale, double width)
        {
            int thickness = (int)(width * scale);

            upBorder.Height = thickness;
            downBorder.Height = thickness;
            leftBorder.Width = thickness;
            rightBorder.Width = thickness;

            upBorder.Fill = new LinearGradientBrush(outer, inner, 90);
            downBorder.Fill = new LinearGradientBrush(inner, outer, 90);
            leftBorder.Fill = new LinearGradientBrush(outer, inner, 0);
            rightBorder.Fill = new LinearGradientBrush(inner, outer, 0);
        }

        int colorIndex = 0;
        double rotation = 0;
        DispatcherOperation? lastInvocation;
        private void DrawingTimer_Tick(object? state)
        {
            if (spectrumData == null)
                return;
            if (lastInvocation != null && 
                lastInvocation.Status == DispatcherOperationStatus.Executing)
                return;

            lastInvocation = Dispatcher.InvokeAsync(() =>
            {
                rotation += .1;
                colorIndex++;

                Color color1 = allColors[colorIndex % allColors.Length];
                Color color2 = allColors[(colorIndex + 200) % allColors.Length];

                double[] bassArea = Visualizer.TakeSpectrumOfFrequency(spectrumData, capture.WaveFormat.SampleRate, 250);
                double bassScale = bassArea.Average() * 100;
                double extraScale = Math.Min(drawingPanel.ActualHeight, drawingPanel.ActualHeight) / 6;

                Brush brush = new SolidColorBrush(Colors.Purple);
                DrawGradientBorder(up, down, left, right, Color.FromArgb(0, color1.R, color1.G, color1.B), color2, bassScale, drawingPanel.ActualWidth / 10);

                DrawGradientStrips(strips, color1, color2, spectrumData, spectrumData.Length, strips.ActualWidth, 0, strips.ActualHeight, 3, -strips.ActualHeight * 10);
                DrawCircleGradientStrips(circle, color1, color2, spectrumData, spectrumData.Length, drawingPanel.ActualHeight / 2, drawingPanel.ActualHeight / 2, Math.Min(drawingPanel.ActualHeight, drawingPanel.ActualHeight) / 4 + extraScale * bassScale, 1, rotation, drawingPanel.ActualHeight / 6 * 10);

                DrawCurve(sampleWave, brush, visualizer.SampleData, visualizer.SampleData.Length, drawingPanel.ActualWidth, 0, drawingPanel.ActualHeight / 2, Math.Min(drawingPanel.ActualHeight / 10, 100));
            });
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            capture.StartRecording();

            //dataTimer = new Timer(DataTimer_Tick, null, 30, 30);
            //drawingTimer = new Timer(DrawingTimer_Tick, null, 30, 30);
        }

        private void Window_Closed(object sender, EventArgs e)
        {
            Environment.Exit(0);
        }
    }
}
