using System;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.RuinGeneration;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace Barotrauma
{
    partial class ScanMission : Mission
    {
        private readonly ContentXElement itemConfig;
        private readonly List<Item> startingItems = new List<Item>();
        private readonly List<Scanner> scanners = new List<Scanner>();
        private readonly Dictionary<Item, ushort> parentInventoryIDs = new Dictionary<Item, ushort>();
        private readonly Dictionary<Item, int> inventorySlotIndices = new Dictionary<Item, int>();
        private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
        private readonly int targetsToScan;
        private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
        private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
        private readonly float minTargetDistance;
        
        private Ruin TargetRuin { get; set; }

        public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
        {
            get
            {
                if (AllTargetsScanned())
                {
                    return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
                }
                else
                {
                    return scanTargets
                        .Where(kvp => !kvp.Value)
                        .Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition));
                }             
            }
        }

        public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
        {
            itemConfig = prefab.ConfigElement.GetChildElement("Items");
            targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1);
            minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f);
        }

        protected override void StartMissionSpecific(Level level)
        {
            Reset();

            if (IsClient) { return; }

            if (itemConfig == null)
            {
                DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set",
                    contentPackage: Prefab.ContentPackage);
                return;
            }

            foreach (var element in itemConfig.Elements())
            {
                LoadItem(element, null);
            }
            GetScanners();

            TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient);
            if (TargetRuin == null)
            {
                DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins",
                    contentPackage: Prefab.ContentPackage);
                return;
            }

            var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false);
            ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null);
            if (ruinWaypoints.Count < targetsToScan)
            {
                DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})",
                    contentPackage: Prefab.ContentPackage);
                return;
            }
            var availableWaypoints = new List<WayPoint>();
            float minTargetDistanceSquared = minTargetDistance * minTargetDistance;
            for (int tries = 0; tries < 15; tries++)
            {
                scanTargets.Clear();
                availableWaypoints.Clear();
                availableWaypoints.AddRange(ruinWaypoints);
                for (int i = 0; i < targetsToScan; i++)
                {
                    var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient);
                    scanTargets.Add(selectedWaypoint, false);
                    availableWaypoints.Remove(selectedWaypoint);
                    if (i < (targetsToScan - 1))
                    {
                        availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull);
                        availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared);
                        if (availableWaypoints.None())
                        {
#if DEBUG
                            DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})",
                                contentPackage: Prefab.ContentPackage);
#endif
                            break;
                        }
                    }
                }
                if (scanTargets.Count >= targetsToScan)
                {
#if DEBUG
                    DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green);
#endif
                    break;
                }
                if ((tries + 1) % 5 == 0)
                {
                    float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance;
                    minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance;
#if DEBUG
                    DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow);
#endif
                }
            }
            if (scanTargets.Count < targetsToScan)
            {
                DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})", 
                    contentPackage: Prefab.ContentPackage);
            }
        }

        private void Reset()
        {
            startingItems.Clear();
            parentInventoryIDs.Clear();
            inventorySlotIndices.Clear();
            parentItemContainerIndices.Clear();
            scanners.Clear();
            TargetRuin = null;
            scanTargets.Clear();
        }

        private void LoadItem(XElement element, Item parent)
        {
            var itemPrefab = FindItemPrefab(element);
            Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub);
            if (!position.HasValue) { return; }
            var item = new Item(itemPrefab, position.Value, cargoRoomSub);
            item.FindHull();
            startingItems.Add(item);
            if (parent?.GetComponent<ItemContainer>() is ItemContainer itemContainer)
            {
                parentInventoryIDs.Add(item, parent.ID);
                parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer));
                parent.Combine(item, user: null);
                inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1);
            }
            foreach (XElement subElement in element.Elements())
            {
                int amount = subElement.GetAttributeInt("amount", 1);
                for (int i = 0; i < amount; i++)
                {
                    LoadItem(subElement, item);
                }
            }
        }

        protected override void MissionStateChanged(int previousState)
        {
            // detect successful scanned targets increasing after scan is completed
            if (previousState < State)
            {
#if CLIENT
                SteamTimelineManager.OnScanSuccessful(this);
#endif
            }
        }

        private void GetScanners()
        {
            foreach (var startingItem in startingItems)
            {
                if (startingItem.GetComponent<Scanner>() is Scanner scanner)
                {
                    scanner.OnScanStarted += OnScanStarted;
                    if (!IsClient)
                    {
                        scanner.OnScanCompleted += OnScanCompleted;
                    }
                    scanners.Add(scanner);
                }
            }
        }

        private void OnScanStarted(Scanner scanner)
        {
            float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius;
            foreach (var kvp in scanTargets)
            {
                if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; }
                scanner.DisplayProgressBar = true;
                break;
            }
        }

        private void OnScanCompleted(Scanner scanner)
        {
            if (IsClient) { return; }
            newTargetsScanned.Clear();
            float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius;
            foreach (var kvp in scanTargets)
            {
                if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; }
                newTargetsScanned.Add(kvp.Key);
            }
            foreach (var wp in newTargetsScanned)
            {
                scanTargets[wp] = true;
            }
#if SERVER
            // Server should make sure that the clients' scan target status is in-sync
            GameMain.Server?.UpdateMissionState(this);
#endif
        }

        private static bool IsValidScanPosition(Scanner scanner, KeyValuePair<WayPoint, bool> scanStatus, float scanRadiusSquared)
        {
            if (scanStatus.Value) { return false; }
            if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; }
            if (Vector2.DistanceSquared(scanStatus.Key.WorldPosition, scanner.Item.WorldPosition) > scanRadiusSquared) { return false; }
            return true;
        }

        protected override void UpdateMissionSpecific(float deltaTime)
        {
            if (IsClient) { return; }
            // Allow the state to be set higher with MissionStateAction, but not lower.
            State = Math.Max(State, scanTargets.Count(kvp => kvp.Value));
        }
        
        private bool AllTargetsScanned() => State >= targetsToScan;
        
        protected override bool DetermineCompleted() => AllTargetsScanned();

        protected override void EndMissionSpecific(bool completed)
        {
            foreach (var scanner in scanners)
            {
                if (scanner.Item is { Removed: false })
                {
                    scanner.OnScanStarted -= OnScanStarted;
                    scanner.OnScanCompleted -= OnScanCompleted;
                    scanner.Item.Remove();
                }
            }
            Reset();
            failed = !completed;
        }
    }
}