using CommunityToolkit.Mvvm.Input;
using Harmonia.Core.Caching;
using Harmonia.Core.Engine;
using Harmonia.Core.Imaging;
using Harmonia.Core.Models;
using Harmonia.Core.Player;
using Harmonia.Core.Playlists;
using Harmonia.Core.Scanner;
using Harmonia.WinUI.Caching;
using Harmonia.WinUI.Storage;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Input;
using Windows.ApplicationModel.DataTransfer;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using Timer = System.Timers.Timer;

namespace Harmonia.WinUI.ViewModels;

public partial class PlaylistViewModel : ViewModelBase
{
    private readonly IAudioPlayer _audioPlayer;
    private readonly IAudioImageCache _audioImageCache;
    private readonly IAudioBitmapImageCache _audioBitmapImageCache;
    private readonly IAudioFileScanner _audioFileScanner;
    private readonly IAudioEngine _audioEngine;
    private readonly IStorageProvider _storageProvider;
    private readonly DispatcherQueue _dispatcherQueue;
    private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];

    private Timer? _filterTimer;

    public Playlist? Playlist { get; private set; }

    private PlaylistSong? _playingSong;
    public PlaylistSong? PlayingSong
    {
        get
        {
            return _playingSong;
        }
        set
        {
            SetProperty(ref _playingSong, value);
        }
    }

    private ObservableCollection<PlaylistSong> _playlistSongs = [];
    public ObservableCollection<PlaylistSong> PlaylistSongs
    {
        get
        {
            return _playlistSongs;
        }
        set
        {
            SetProperty(ref _playlistSongs, value);
        }
    }

    private string? _filter;
    public string? Filter
    {
        get
        {
            return _filter;
        }
        set
        {
            SetProperty(ref _filter, value);
            RestartFilterTimer();
        }
    }

    private ObservableCollection<PlaylistSong> _filteredPlaylistSongs = [];
    public ObservableCollection<PlaylistSong> FilteredPlaylistSongs
    {
        get
        {
            return _filteredPlaylistSongs;
        }
        set
        {
            SetProperty(ref _filteredPlaylistSongs, value);
        }
    }

    private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
    public ObservableCollection<PlaylistSong> SelectedPlaylistSongs
    {
        get
        {
            return _selectedPlaylistSongs;
        }
        set
        {
            SetProperty(ref _selectedPlaylistSongs, value);
        }
    }

    public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected);
    public ICommand AddFilesCommand => new AsyncRelayCommand(AddFilesAsync);
    public ICommand AddFolderCommand => new AsyncRelayCommand(AddFolderAsync);
    public ICommand RemoveSongsCommand => new RelayCommand(RemoveSongs, AreSongsSelected);
    public ICommand CutSongsCommand => new RelayCommand(CutSongs, AreSongsSelected);
    public ICommand CopySongsCommand => new RelayCommand(CopySongs, AreSongsSelected);
    public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
    public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
    public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
    public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
    public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);

    public bool IsUserUpdating { get; set; }
    private bool _isUserInitiatingSongChange;

    public event EventHandler? PlayingSongChangedAutomatically;

    public PlaylistViewModel(
        IAudioPlayer audioPlayer,
        IAudioImageCache audioImageCache,
        IAudioBitmapImageCache audioBitmapImageCache,
        IAudioFileScanner audioFileScanner,
        IAudioEngine audioEngine,
        IStorageProvider storageProvider,
        IPlaylistRepository playlistRepository)
    {
        _audioPlayer = audioPlayer;
        _audioPlayer.PlaylistChanged += OnPlaylistChanged;
        _audioPlayer.PlayingSongChanged += OnPlayingSongChanged;

        _audioImageCache = audioImageCache;
        _audioBitmapImageCache = audioBitmapImageCache;
        _audioFileScanner = audioFileScanner;
        _audioEngine = audioEngine;
        _storageProvider = storageProvider;
        _dispatcherQueue = DispatcherQueue.GetForCurrentThread();

        FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged;

        // Testing
        Task.Run(() => PlayDemoSong(playlistRepository));
    }

    private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        if (IsUserUpdating == false)
            return;

        int x = 1;
    }

    private async Task PlayDemoSong(IPlaylistRepository playlistRepository)
    {
        if (playlistRepository.Get().Count == 0)
        {
            playlistRepository.AddPlaylist();
        }

        Playlist playlist = playlistRepository.Get().First();

        if (playlist.Songs.Count > 0)
            await _audioPlayer.LoadAsync(playlist.Songs[0], PlaybackMode.LoadOnly);
    }

    private void OnPlaylistChanged(object? sender, EventArgs e)
    {
        if (Playlist != null)
        {
            Playlist.PlaylistUpdated -= OnPlaylistUpdated;
        }

        Playlist = _audioPlayer.Playlist;

        if (Playlist != null)
        {
            Playlist.PlaylistUpdated += OnPlaylistUpdated;
        }

        PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];

        PlaylistSongs = [.. playlistSongs];
        UpdateFilteredSongs();
    }

    private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
    {
        if (IsUserUpdating)
            return;

        switch (e.Action)
        {
            case PlaylistUpdateAction.Add:
                _dispatcherQueue.TryEnqueue(() => AddSongs(e.Songs, e.Index));
                break;
            case PlaylistUpdateAction.Remove:
                _dispatcherQueue.TryEnqueue(() => RemoveSongsFromCollection(e.Songs));
                break;
        }
    }

    private void AddSongs(PlaylistSong[] playlistSongs, int index = 0)
    {
        // TODO: Performance improvements
        int currentIndex = index;

        foreach (PlaylistSong playlistSong in playlistSongs)
        {
            PlaylistSongs.Insert(currentIndex++, playlistSong);
        }

        UpdateFilteredSongs();
    }

    private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
    {
        foreach (PlaylistSong playlistSong in playlistSongs)
        {
            PlaylistSongs.Remove(playlistSong);
        }

        UpdateFilteredSongs();
    }

    private void OnPlayingSongChanged(object? sender, EventArgs e)
    {
        PlayingSong = _audioPlayer.PlayingSong;

        if (_isUserInitiatingSongChange)
        {
            _isUserInitiatingSongChange = false;
        }
        else
        {
            PlayingSongChangedAutomatically?.Invoke(this, EventArgs.Empty);
        }
    }

    public async Task PlaySongAsync(PlaylistSong playlistSong)
    {
        _isUserInitiatingSongChange = true;
        await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay);
    }

    public async Task<SongPictureInfo?> GetSongPictureInfoAsync(int hashCode, PlaylistSong playlistSong)
    {
        _imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
        cancellationTokenSource?.Cancel();

        cancellationTokenSource = new();
        _imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource);

        return await _audioImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token);
    }

    public async Task<BitmapImage?> GetBitmapImageAsync(int hashCode, PlaylistSong playlistSong)
    {
        _imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
        cancellationTokenSource?.Cancel();

        cancellationTokenSource = new();
        _imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource);

        return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token);
    }

    public async Task<BitmapImage?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken)
    {
        return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken);
    }

    #region Filtering

    private void RestartFilterTimer()
    {
        if (_filterTimer == null)
        {
            _filterTimer = new Timer(300);
            _filterTimer.Elapsed += OnFilterTimerElapsed;
            _filterTimer.Start();
        }
        else
        {
            _filterTimer.Interval = 300;
        }
    }

    private void OnFilterTimerElapsed(object? sender, ElapsedEventArgs e)
    {
        if (_filterTimer == null)
            return;

        _filterTimer.Stop();
        _filterTimer.Dispose();
        _filterTimer = null;

        _dispatcherQueue.TryEnqueue(UpdateFilteredSongs);
    }

    private void UpdateFilteredSongs()
    {
        if (Playlist == null)
            return;

        List<PlaylistSong> filteredPlaylistSongs = [.. Playlist.Songs.Where(playlistSong => IsFiltered(playlistSong.Song))];
        //FilteredPlaylistSongs = [.. filteredPlaylistSongs];

        for (int i = FilteredPlaylistSongs.Count - 1; i >= 0; i--)
        {
            PlaylistSong playlistSong = FilteredPlaylistSongs[i];

            bool inPlaylist = Playlist.Songs.Contains(playlistSong);
            bool inFilter = filteredPlaylistSongs.Contains(playlistSong);

            if (!inPlaylist || !inFilter)
            {
                FilteredPlaylistSongs.Remove(playlistSong);
            }
        }

        int insertionIndex = 0;

        foreach (PlaylistSong playlistSong in Playlist.Songs)
        {
            bool inFilter = filteredPlaylistSongs.Contains(playlistSong);
            bool inCurrentFilteredList = FilteredPlaylistSongs.Contains(playlistSong);

            if (inFilter)
            {
                if (!inCurrentFilteredList)
                {
                    FilteredPlaylistSongs.Insert(insertionIndex, playlistSong);
                }

                insertionIndex++;
            }
        }
    }

    private bool IsFiltered(Song song)
    {
        if (string.IsNullOrWhiteSpace(Filter))
            return true;

        var shortFileName = Path.GetFileName(song.FileName);

        if (shortFileName.Contains(Filter, StringComparison.OrdinalIgnoreCase))
            return true;

        if (string.IsNullOrWhiteSpace(song.Title) == false && song.Title.Contains(Filter, StringComparison.OrdinalIgnoreCase))
            return true;

        if (string.IsNullOrWhiteSpace(song.Album) == false && song.Album.Contains(Filter, StringComparison.OrdinalIgnoreCase))
            return true;

        if (song.AlbumArtists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase)))
            return true;

        if (song.Artists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase)))
            return true;

        return false;
    }

    #endregion

    #region Commands

    private async Task PlaySongAsync()
    {
        if (SelectedPlaylistSongs.Count == 0)
            return;

        await _audioPlayer.LoadAsync(SelectedPlaylistSongs[0], PlaybackMode.LoadAndPlay);
    }

    private bool AreSongsSelected()
    {
        return SelectedPlaylistSongs.Count > 0;
    }

    private async Task AddFilesAsync(CancellationToken cancellationToken)
    {
        if (Playlist == null)
            return;

        FilePickerOptions filePickerOptions = new()
        {
            FileTypeFilter = [GetAudioFileTypes()],
        };

        string[] fileNames = await _storageProvider.GetFilesAsync(filePickerOptions);
        Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);

        Playlist.AddSongs(songs);
    }

    private FilePickerFileType GetAudioFileTypes()
    {
        string[] patterns = [.. _audioEngine.SupportedFormats.Select(format => format.Replace("*", ""))];

        return new()
        {
            Name = "Audio Files",
            Patterns = patterns
        };
    }

    private async Task AddFolderAsync(CancellationToken cancellationToken)
    {
        if (Playlist == null)
            return;

        string? path = await _storageProvider.GetPathAsync();

        if (string.IsNullOrWhiteSpace(path))
            return;

        Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken);
        Playlist.AddSongs(songs);
    }

    public async Task AddFilesAsync(string[] fileNames, CancellationToken cancellationToken)
    {
        if (Playlist == null)
            return;

        Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);
        Playlist.AddSongs(songs);
    }

    public async Task AddFolderAsync(string path, CancellationToken cancellationToken)
    {
        if (Playlist == null)
            return;

        Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken);
        Playlist.AddSongs(songs);
    }

    private void RemoveSongs()
    {
        if (Playlist == null)
            return;

        if (SelectedPlaylistSongs.Count == 0)
            return;

        PlaylistSong[] playlistSongs = [.. SelectedPlaylistSongs];

        Playlist.RemoveSongs(playlistSongs);
    }

    private void CutSongs()
    {
        if (SelectedPlaylistSongs.Count == 0)
            return;

        CopySelectedSongsToClipboard();
    }

    private void CopySongs()
    {
        if (SelectedPlaylistSongs.Count == 0)
            return;

        CopySelectedSongsToClipboard();
    }

    private void CopySelectedSongsToClipboard()
    {
        Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)];

        DataPackage dataPackage = new()
        {
            RequestedOperation = DataPackageOperation.Copy
        };

        dataPackage.Properties.Add("Type", "SongList");
        dataPackage.SetData(StandardDataFormats.Text, JsonSerializer.Serialize(songs));

        Clipboard.SetContent(dataPackage);
    }

    private bool CanPasteSongs()
    {
        if (Playlist == null || SelectedPlaylistSongs.Count == 0)
            return false;

        DataPackageView dataPackageView = Clipboard.GetContent();

        if (dataPackageView == null)
            return false;

        if (dataPackageView.Properties.ContainsKey("Type") == false)
            return false;

        return dataPackageView.Properties["Type"].ToString() == "SongList";
    }

    private async Task PasteSongsAsync()
    {
        if (Playlist == null || SelectedPlaylistSongs.Count == 0)
            return;

        int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]);

        if (selectedPlaylistSongIndex == -1)
            return;

        Song[] songs = await GetSongsFromClipboardAsync();

        Playlist.AddSongs(songs, selectedPlaylistSongIndex + 1);
    }

    private static async Task<Song[]> GetSongsFromClipboardAsync()
    {
        DataPackageView dataPackageView = Clipboard.GetContent();
        string data = await dataPackageView.GetTextAsync(StandardDataFormats.Text);

        return JsonSerializer.Deserialize<Song[]>(data) ?? [];
    }

    private void OpenFileLocation()
    {
        if (SelectedPlaylistSongs.Count == 0)
            return;

        string argument = "/select, \"" + SelectedPlaylistSongs[0].Song.FileName + "\"";
        Process.Start("explorer.exe", argument);
    }

    private void RefreshTags()
    {
        //Playlist?.RefreshTags();
    }

    private void RemoveMissingSongs()
    {
        Playlist?.RemoveMissingSongs();
    }

    private void RemoveDuplicateSongs()
    {
        Playlist?.RemoveDuplicateSongs();
    }

    #endregion
}