using System;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using DryIoc;
using ReactiveUI;
using Serilog;

namespace Artemis.UI.Shared.Routing;

internal class Navigation
{
    private readonly IContainer _container;
    private readonly ILogger _logger;

    private readonly IRoutableHostScreen _root;
    private readonly RouteResolution _resolution;
    private readonly RouterNavigationOptions _options;
    private CancellationTokenSource _cts;

    public Navigation(IContainer container, ILogger logger, IRoutableHostScreen root, RouteResolution resolution, RouterNavigationOptions options)
    {
        _container = container;
        _logger = logger;

        _root = root;
        _resolution = resolution;
        _options = options;
        _cts = new CancellationTokenSource();
    }

    public bool Cancelled => _cts.IsCancellationRequested;
    public bool Completed { get; private set; }

    public async Task Navigate(NavigationArguments args)
    {
        if (_options.EnableLogging)
            _logger.Information("Navigating to {Path}", _resolution.Path);
        _cts = new CancellationTokenSource();
        await NavigateResolution(_resolution, args, _root);

        if (!Cancelled && _options.EnableLogging)
            _logger.Information("Navigated to {Path}", _resolution.Path);
    }

    public void Cancel()
    {
        if (Cancelled || Completed)
            return;

        if (_options.EnableLogging)
            _logger.Information("Cancelled navigation to {Path}", _resolution.Path);
        _cts.Cancel();
    }

    private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableHostScreen host)
    {
        if (Cancelled)
            return;

        // Reuse the screen if its type has not changed, if a new one must be created, don't do so on the UI thread
        IRoutableScreen screen;
        if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel)
            screen = host.InternalScreen;
        else
            screen = await Task.Run(() => resolution.GetViewModel(_container));

        // If resolution has a child, ensure the screen can host it
        if (resolution.Child != null && screen is not IRoutableHostScreen)
            throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableHostScreen)}.");

        // Only change the screen if it wasn't reused
        if (!ReferenceEquals(host.InternalScreen, screen))
        {
            try
            {
                host.InternalChangeScreen(screen);
            }
            catch (Exception e)
            {
                Cancel();
                if (e is not TaskCanceledException)
                    _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
            }
        }

        if (CancelIfRequested(args, "ChangeScreen", screen))
            return;
        
        // Navigate on the screen
        args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>();
        try
        {
            await screen.InternalOnNavigating(args, _cts.Token);
        }
        catch (Exception e)
        {
            Cancel();
            if (e is not TaskCanceledException)
                _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
        }

        if (CancelIfRequested(args, "OnNavigating", screen))
            return;

        if (screen is IRoutableHostScreen childScreen)
        {
            // Navigate the child too
            if (resolution.Child != null)
                await NavigateResolution(resolution.Child, args, childScreen);
            // Make sure there is no child
            else if (childScreen.InternalScreen != null)
                childScreen.InternalChangeScreen(null);
        }


        Completed = true;
    }

    public bool PathEquals(string path, RouterNavigationOptions options)
    {
        return options.PathEquals(_resolution.Path, path);
    }

    private bool CancelIfRequested(NavigationArguments args, string stage, object screen)
    {
        if (Cancelled)
            return true;

        if (!args.Cancelled)
            return false;

        _logger.Debug("Navigation to {Path} during {Stage} by {Screen}", args.Path, stage, screen.GetType().Name);
        Cancel();
        return true;
    }
}