Загрузка данных


using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace NightPlayer
{
    public partial class MediaTrimmerWindow : Window
    {
        // ── State ──────────────────────────────────────────────────
        private readonly string _filePath;
        private readonly TimeSpan _totalDuration;

        private TimeSpan _startTime = TimeSpan.Zero;
        private TimeSpan _endTime   = TimeSpan.Zero;

        // Timeline drag
        private enum DragHandle { None, Start, End }
        private DragHandle _dragging = DragHandle.None;
        private double _timelineWidth; // updated on layout

        // Suppress TextChanged feedback loop while updating from timeline
        private bool _updatingBoxes;

        // ── Init ───────────────────────────────────────────────────
        public MediaTrimmerWindow(string filePath, TimeSpan duration)
        {
            InitializeComponent();
            _filePath      = filePath;
            _totalDuration = duration;
            _endTime       = duration;

            FileNameBlock.Text  = IOPath.GetFileName(filePath);
            TitleBlock.Text     = $"Обрезка — {IOPath.GetFileName(filePath)}";

            // Write initial time boxes without triggering sync
            _updatingBoxes = true;
            StartTimeBox.Text = FormatTime(TimeSpan.Zero);
            EndTimeBox.Text   = FormatTime(duration);
            _updatingBoxes = false;

            Loaded += (_, _) =>
            {
                _timelineWidth = TimelineCanvas.ActualWidth;
                UpdateHandlesFromTimes();
                UpdateDurationLabel();
            };

            TimelineCanvas.SizeChanged += (_, _) =>
            {
                _timelineWidth = TimelineCanvas.ActualWidth;
                UpdateHandlesFromTimes();
            };
        }

        // ── Timeline drag ──────────────────────────────────────────
        private void Timeline_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton != MouseButtonState.Pressed) return;
            _timelineWidth = TimelineCanvas.ActualWidth;

            var pos  = e.GetPosition(TimelineCanvas).X;
            double sx = Canvas.GetLeft(HandleStart);
            double ex = Canvas.GetLeft(HandleEnd);

            // Canvas.GetLeft returns NaN before first layout pass — default to edges
            if (double.IsNaN(sx)) sx = 0;
            if (double.IsNaN(ex)) ex = _timelineWidth;

            // Pick whichever handle is closer
            _dragging = Math.Abs(pos - sx) <= Math.Abs(pos - ex)
                ? DragHandle.Start : DragHandle.End;

            TimelineCanvas.CaptureMouse();
            e.Handled = true;
        }

        private void Timeline_MouseMove(object sender, MouseEventArgs e)
        {
            if (_dragging == DragHandle.None || e.LeftButton != MouseButtonState.Pressed) return;
            if (_timelineWidth <= 0) return;

            double pos = Math.Clamp(e.GetPosition(TimelineCanvas).X, 0, _timelineWidth);
            double ratio = pos / _timelineWidth;
            var t = TimeSpan.FromSeconds(ratio * _totalDuration.TotalSeconds);

            if (_dragging == DragHandle.Start)
            {
                _startTime = t < _endTime - TimeSpan.FromSeconds(1) ? t : _endTime - TimeSpan.FromSeconds(1);
                if (_startTime < TimeSpan.Zero) _startTime = TimeSpan.Zero;
            }
            else
            {
                _endTime = t > _startTime + TimeSpan.FromSeconds(1) ? t : _startTime + TimeSpan.FromSeconds(1);
                if (_endTime > _totalDuration) _endTime = _totalDuration;
            }

            UpdateHandlesFromTimes();
            SyncBoxesFromTimes();
            UpdateDurationLabel();
        }

        private void Timeline_MouseUp(object sender, MouseButtonEventArgs e)
        {
            _dragging = DragHandle.None;
            TimelineCanvas.ReleaseMouseCapture();
        }

        // ── Time text boxes ────────────────────────────────────────
        private void TimeBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (_updatingBoxes) return;
            if (StartTimeBox == null || EndTimeBox == null) return;

            if (TryParseTime(StartTimeBox.Text, out var s) &&
                TryParseTime(EndTimeBox.Text,   out var en))
            {
                if (s >= TimeSpan.Zero && en <= _totalDuration && s < en)
                {
                    _startTime = s;
                    _endTime   = en;
                    UpdateHandlesFromTimes();
                    UpdateDurationLabel();
                    StartTimeBox.Foreground = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
                    EndTimeBox.Foreground   = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
                }
                else
                {
                    ((TextBox)sender).Foreground = new SolidColorBrush(Color.FromRgb(0xCC, 0x00, 0x00));
                }
            }
            else
            {
                ((TextBox)sender).Foreground = new SolidColorBrush(Color.FromRgb(0xCC, 0x00, 0x00));
            }
        }

        // ── UI helpers ─────────────────────────────────────────────
        private void UpdateHandlesFromTimes()
        {
            if (_timelineWidth <= 0 || _totalDuration.TotalSeconds <= 0) return;

            double trackW = _timelineWidth;
            double sx = (_startTime.TotalSeconds / _totalDuration.TotalSeconds) * trackW;
            double ex = (_endTime.TotalSeconds   / _totalDuration.TotalSeconds) * trackW;

            // Clamp handle to track
            sx = Math.Clamp(sx, 0, trackW - 12);
            ex = Math.Clamp(ex, 12, trackW);

            Canvas.SetLeft(HandleStart, sx);
            Canvas.SetLeft(HandleEnd,   ex - 12); // right edge of handle at ex

            // Selection highlight
            Canvas.SetLeft(SelectionHighlight, sx);
            SelectionHighlight.Width = Math.Max(0, ex - sx);
        }

        private void SyncBoxesFromTimes()
        {
            _updatingBoxes = true;
            StartTimeBox.Text = FormatTime(_startTime);
            EndTimeBox.Text   = FormatTime(_endTime);
            StartTimeBox.Foreground = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
            EndTimeBox.Foreground   = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
            _updatingBoxes = false;
        }

        private void UpdateDurationLabel()
        {
            var dur = _endTime - _startTime;
            DurationLabel.Text = $"Длительность фрагмента: {FormatTime(dur)}";
        }

        private static string FormatTime(TimeSpan t)
            => $"{(int)t.TotalHours:D2}:{t.Minutes:D2}:{t.Seconds:D2}";

        private static bool TryParseTime(string s, out TimeSpan result)
        {
            result = TimeSpan.Zero;
            if (string.IsNullOrWhiteSpace(s)) return false;
            var parts = s.Split(':');
            if (parts.Length == 3 &&
                int.TryParse(parts[0], out int h) &&
                int.TryParse(parts[1], out int m) &&
                int.TryParse(parts[2], out int sec))
            {
                result = new TimeSpan(h, m, sec);
                return true;
            }
            return false;
        }

        // ── Trim action ────────────────────────────────────────────
        private async void Trim_Click(object sender, RoutedEventArgs e)
        {
            // Validate selection
            if (_endTime <= _startTime || (_endTime - _startTime).TotalSeconds < 1)
            {
                MessageBox.Show("Выделите фрагмент длиной не менее 1 секунды.",
                    "Обрезка", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            // Ask what to do with original
            var choice = MessageBox.Show(
                "Перезаписать оригинальный файл?\n\n" +
                "Да — перезаписать оригинал\n" +
                "Нет — сохранить как новый файл",
                "Обрезка",
                MessageBoxButton.YesNoCancel,
                MessageBoxImage.Question);

            if (choice == MessageBoxResult.Cancel) return;

            string outputPath;
            if (choice == MessageBoxResult.Yes)
            {
                // Save to temp, then replace original
                outputPath = IOPath.Combine(
                    IOPath.GetDirectoryName(_filePath)!,
                    $"__trim_tmp_{IOPath.GetFileName(_filePath)}");
            }
            else
            {
                // Save As dialog
                var ext = IOPath.GetExtension(_filePath);
                var dlg = new Microsoft.Win32.SaveFileDialog
                {
                    Title      = "Сохранить обрезанный файл",
                    Filter     = $"Медиафайл (*{ext})|*{ext}|Все файлы (*.*)|*.*",
                    DefaultExt = ext,
                    FileName   = IOPath.GetFileNameWithoutExtension(_filePath) + "_trimmed"
                };
                if (dlg.ShowDialog() != true) return;
                outputPath = dlg.FileName;
            }

            BtnTrim.IsEnabled   = false;
            BtnCancel.IsEnabled = false;
            BtnTrim.Content     = "Обрезка...";

            bool ok = false;
            string error = "";

            try
            {
                ok = await TrimMediaAsync(_filePath, outputPath, _startTime, _endTime,
                    msg => Dispatcher.Invoke(() => BtnTrim.Content = msg));
            }
            catch (Exception ex)
            {
                error = ex.Message;
            }

            BtnTrim.IsEnabled   = true;
            BtnCancel.IsEnabled = true;
            BtnTrim.Content     = "✂  Обрезать";

            if (!ok)
            {
                if (File.Exists(outputPath) && outputPath.Contains("__trim_tmp_"))
                    File.Delete(outputPath);

                MessageBox.Show(
                    string.IsNullOrEmpty(error)
                        ? "Не удалось обрезать файл. Формат может не поддерживаться."
                        : $"Ошибка обрезки:\n{error}",
                    "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            // Overwrite original if needed
            if (choice == MessageBoxResult.Yes)
            {
                try
                {
                    File.Delete(_filePath);
                    File.Move(outputPath, _filePath);
                }
                catch (Exception ex)
                {
                    MessageBox.Show($"Файл обрезан, но не удалось заменить оригинал:\n{ex.Message}",
                        "Предупреждение", MessageBoxButton.OK, MessageBoxImage.Warning);
                    return;
                }
            }

            MessageBox.Show("Файл успешно обрезан!", "Обрезка", MessageBoxButton.OK, MessageBoxImage.Information);
            DialogResult = true;
            Close();
        }

        private static readonly HashSet<string> AudioExtensions =
            new(StringComparer.OrdinalIgnoreCase) { ".mp3", ".wav", ".flac", ".ogg", ".aiff", ".aif", ".wma", ".m4a", ".aac" };

        private static readonly HashSet<string> VideoExtensions =
            new(StringComparer.OrdinalIgnoreCase) { ".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm", ".m4v", ".ts" };

        private static async Task<bool> TrimMediaAsync(
            string input, string output, TimeSpan start, TimeSpan end,
            Action<string>? progress = null)
        {
            string ext = IOPath.GetExtension(input).ToLowerInvariant();

            // ── Audio: use NAudio (no external tools needed) ──
            if (AudioExtensions.Contains(ext))
                return await TrimAudioWithNAudioAsync(input, output, start, end);

            // ── Video: use Xabe.FFmpeg with auto-download ──
            if (VideoExtensions.Contains(ext))
                return await TrimVideoWithXabeAsync(input, output, start, end, progress);

            return false;
        }

        private static Task<bool> TrimAudioWithNAudioAsync(
            string input, string output, TimeSpan start, TimeSpan end)
        {
            return Task.Run(() =>
            {
                try
                {
                    using var reader = new NAudio.Wave.AudioFileReader(input);
                    reader.CurrentTime = start;
                    var duration = end - start;

                    // Always write as WAV — rename output extension if needed
                    string wavOutput = System.IO.Path.ChangeExtension(output, ".wav");
                    using (var writer = new NAudio.Wave.WaveFileWriter(wavOutput, reader.WaveFormat))
                    {
                        var buffer = new byte[reader.WaveFormat.AverageBytesPerSecond];
                        long bytesToRead = (long)(duration.TotalSeconds * reader.WaveFormat.AverageBytesPerSecond);

                        while (bytesToRead > 0)
                        {
                            int toRead = (int)Math.Min(buffer.Length, bytesToRead);
                            int read   = reader.Read(buffer, 0, toRead);
                            if (read == 0) break;
                            writer.Write(buffer, 0, read);
                            bytesToRead -= read;
                        }
                    }

                    // If caller wanted a different extension (e.g. .mp3), rename the wav
                    if (!string.Equals(wavOutput, output, StringComparison.OrdinalIgnoreCase))
                    {
                        if (File.Exists(output)) File.Delete(output);
                        File.Move(wavOutput, output);
                    }

                    return true;
                }
                catch
                {
                    return false;
                }
            });
        }

        private static async Task<bool> TrimVideoWithXabeAsync(
            string input, string output, TimeSpan start, TimeSpan end,
            Action<string>? progress)
        {
            string exeDir = System.IO.Path.GetDirectoryName(
                System.Reflection.Assembly.GetExecutingAssembly().Location) ?? ".";
            Xabe.FFmpeg.FFmpeg.SetExecutablesPath(exeDir);

            bool isWindows = System.Runtime.InteropServices.RuntimeInformation
                .IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
            string ffmpegExe = System.IO.Path.Combine(exeDir,
                isWindows ? "ffmpeg.exe" : "ffmpeg");

            if (!File.Exists(ffmpegExe))
            {
                progress?.Invoke("Загрузка FFmpeg...");
                try
                {
                    await Xabe.FFmpeg.Downloader.FFmpegDownloader.GetLatestVersion(
                        Xabe.FFmpeg.Downloader.FFmpegVersion.Official, exeDir);
                }
                catch (Exception ex)
                {
                    throw new Exception(
                        $"FFmpeg не найден в папке с программой и не удалось скачать автоматически.\n" +
                        $"Положите ffmpeg.exe рядом с NightPlayer.exe\n\nДеталь: {ex.Message}");
                }
            }

            if (!File.Exists(ffmpegExe))
                throw new Exception(
                    $"ffmpeg.exe не найден по пути:\n{ffmpegExe}\n\n" +
                    "Скачайте ffmpeg.exe с https://ffmpeg.org/download.html и положите рядом с NightPlayer.exe");

            progress?.Invoke("Обрезка...");
            double durationSec = (end - start).TotalSeconds;

            try
            {
                var ic = System.Globalization.CultureInfo.InvariantCulture;
                var conversion = Xabe.FFmpeg.FFmpeg.Conversions.New()
                    .AddParameter(
                        $"-ss {start.TotalSeconds.ToString("F3", ic)} -t {durationSec.ToString("F3", ic)} " +
                        $"-i \"{input}\" -c copy \"{output}\"");

                await conversion.Start();
                return true;
            }
            catch (Exception ex)
            {
                throw new Exception($"FFmpeg завершился с ошибкой:\n{ex.Message}");
            }
        }

        // ── Window controls ────────────────────────────────────────
        private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left) DragMove();
        }

        private void Close_Click(object sender, RoutedEventArgs e)
        {
            DialogResult = false;
            Close();
        }

        private void Cancel_Click(object sender, RoutedEventArgs e)
        {
            DialogResult = false;
            Close();
        }
    }

    // Alias to avoid ambiguity with System.Windows.Shapes.Path
    file static class IOPath
    {
        public static string GetFileName(string p)           => System.IO.Path.GetFileName(p);
        public static string GetFileNameWithoutExtension(string p) => System.IO.Path.GetFileNameWithoutExtension(p);
        public static string GetExtension(string p)          => System.IO.Path.GetExtension(p);
        public static string? GetDirectoryName(string p)     => System.IO.Path.GetDirectoryName(p);
        public static string Combine(string a, string b)     => System.IO.Path.Combine(a, b);
    }
}