using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.Networking;

namespace Barotrauma
{
    class TraitorMissionPrefab
    {
        public class TraitorMissionEntry : Prefab
        {
            public static PrefabCollection<TraitorMissionEntry> Prefabs => TraitorMissionPrefab.Prefabs;
            
            public readonly TraitorMissionPrefab Prefab;
            public float SelectedWeight;

            public TraitorMissionEntry(ContentXElement element, TraitorMissionsFile file) : base(file, element)
            {
                Prefab = new TraitorMissionPrefab(element);
            }

            public override void Dispose() { }
        }
        public static readonly PrefabCollection<TraitorMissionEntry> Prefabs = new PrefabCollection<TraitorMissionEntry>();

        public static TraitorMissionPrefab RandomPrefab()
        {
            var selected = ToolBox.SelectWeightedRandom(Prefabs.ToList(), Prefabs.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random);
            //the weight of the missions that didn't get selected keeps growing the make them more likely to get picked
            foreach (var mission in Prefabs)
            {
                mission.SelectedWeight += 10;
            }
            selected.SelectedWeight = 0.0f;
            return selected.Prefab;
        }

        private class AttributeChecker : IDisposable
        {
            private readonly XElement element;
            private readonly HashSet<string> required = new HashSet<string>();
            private readonly HashSet<string> optional = new HashSet<string>();

            public void Optional(params string[] names)
            {
                optional.UnionWith(names);
            }

            public void Required(params string[] names)
            {
                required.UnionWith(names);
            }

            public void Dispose()
            {
                foreach (var requiredName in required)
                {
                    if (element.Attributes().All(attribute => attribute.Name != requiredName))
                    {
                        GameServer.Log($"Required attribute \"{requiredName}\" is missing in \"{element.Name}\"", ServerLog.MessageType.Error);
                    }
                }
                foreach (var attribute in element.Attributes())
                {
                    var attributeName = attribute.Name.ToString();
                    if (!required.Contains(attributeName) && !optional.Contains(attributeName))
                    {
                        GameServer.Log($"Unsupported attribute \"{attributeName}\" in \"{element.Name}\"", ServerLog.MessageType.Error);
                    }
                }
            }

            public AttributeChecker(XElement element)
            {
                this.element = element;
            }
        }

        public class Goal
        {
            public readonly string Type;
            public readonly XElement Config;

            public Goal(string type, XElement config)
            {
                Type = type;
                Config = config;
            }

            private delegate bool TargetFilter(string value, Character character);
            private static Dictionary<string, TargetFilter> targetFilters = new Dictionary<string, TargetFilter>()
            {
                { "job", (value, character) => value == character.Info.Job.Prefab.Identifier },
                { "role", (value, character) => value.Equals(GameMain.Server.TraitorManager.GetTraitorRole(character), StringComparison.OrdinalIgnoreCase) }
            };

            public Traitor.Goal Instantiate()
            {
                Traitor.Goal goal = null;
                using (var checker = new AttributeChecker(Config))
                {
                    checker.Required("type");
                    var goalType = Config.GetAttributeString("type", "");
                    switch (goalType.ToLowerInvariant())
                    {
                        case "killtarget":
                        {
                            checker.Optional(targetFilters.Keys.ToArray());
                            checker.Optional("causeofdeath");
                            checker.Optional("affliction");
                            checker.Optional("roomname");
                            checker.Optional("targetcount");
                            checker.Optional("targetpercentage");
                            List<Traitor.TraitorMission.CharacterFilter> killFilters = new List<Traitor.TraitorMission.CharacterFilter>();
                            foreach (var attribute in Config.Attributes())
                            {
                                if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
                                {
                                    killFilters.Add((character) => filter(attribute.Value, character));
                                }
                            }
                            goal = new Traitor.GoalKillTarget((character) => killFilters.All(f => f(character)), 
                                (CauseOfDeathType)Enum.Parse(typeof(CauseOfDeathType), Config.GetAttributeString("causeofdeath", "Unknown"), true),
                                Config.GetAttributeString("affliction", null), Config.GetAttributeString("targethull", null), Config.GetAttributeInt("targetcount", -1), 
                                Config.GetAttributeFloat("targetpercentage", -1f));
                            break;
                        }
                        case "destroyitems":
                        {
                            checker.Required("tag");
                            checker.Optional("percentage", "matchIdentifier", "matchTag", "matchInventory");
                            var tag = Config.GetAttributeString("tag", null);
                            if (tag != null)
                            {
                                goal = new Traitor.GoalDestroyItemsWithTag(
                                    tag,
                                    Config.GetAttributeFloat("percentage", 100.0f) / 100.0f,
                                    Config.GetAttributeBool("matchIdentifier", true),
                                    Config.GetAttributeBool("matchTag", true),
                                    Config.GetAttributeBool("matchInventory", false));
                            }
                            break;
                        }
                        case "sabotage":
                        {
                            checker.Required("tag");
                            checker.Optional("threshold");
                            var tag = Config.GetAttributeString("tag", null);
                            if (tag != null)
                            {
                                goal = new Traitor.GoalSabotageItems(tag, Config.GetAttributeFloat("threshold", 20.0f));
                            }
                            break;
                        }
                        case "floodsub":
                            checker.Optional("percentage");
                            goal = new Traitor.GoalFloodPercentOfSub(Config.GetAttributeFloat("percentage", 100.0f) / 100.0f);
                            break;
                        case "finditem":
                            checker.Required("identifier");
                            checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers", "percentage");
                            List<Traitor.TraitorMission.CharacterFilter> itemCountFilters = new List<Traitor.TraitorMission.CharacterFilter>();
                            foreach (var attribute in Config.Attributes())
                            {
                                if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
                                {
                                    itemCountFilters.Add((character) => filter(attribute.Value, character));
                                }
                            }
                            goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)),
                                Config.GetAttributeString("identifier",
                                    null),
                                Config.GetAttributeBool("preferNew",
                                    true),
                                Config.GetAttributeBool("allowNew",
                                    true),
                                Config.GetAttributeBool("allowExisting",
                                    true),
                                Config.GetAttributeFloat("percentage",
                                    -1f),
                                Config.GetAttributeIdentifierArray("allowedContainers",
                                    new string[]
                                    {
                                        "steelcabinet",
                                        "mediumsteelcabinet",
                                        "suppliescabinet"
                                    }.ToIdentifiers()));
                            break;
                        case "replaceinventory":
                            checker.Required("containers", "replacements");
                            checker.Optional("percentage");
                            goal = new Traitor.GoalReplaceInventory(Config.GetAttributeIdentifierArray("containers", new Identifier[] { }), Config.GetAttributeIdentifierArray("replacements", new Identifier[] { }), Config.GetAttributeFloat("percentage", 100.0f) / 100.0f);
                            break;
                        case "reachdistancefromsub":
                            checker.Optional("distance");
                            goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 125f));
                            break;
                        case "injectpoison":
                            checker.Optional(targetFilters.Keys.ToArray());
                            checker.Required("poison");
                            checker.Required("affliction");
                            checker.Optional("targetcount");
                            checker.Optional("targetpercentage");
                            List<Traitor.TraitorMission.CharacterFilter> poisonFilters = new List<Traitor.TraitorMission.CharacterFilter>();
                            foreach (var attribute in Config.Attributes())
                            {
                                if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter))
                                {
                                    poisonFilters.Add((character) => filter(attribute.Value, character));
                                }
                            }
                            goal = new Traitor.GoalInjectTarget((character) => poisonFilters.All(f => f(character)), Config.GetAttributeString("poison", null), 
                                Config.GetAttributeString("affliction", null), Config.GetAttributeInt("targetcount", -1), Config.GetAttributeFloat("targetpercentage", -1f));
                            break;
                        case "unwire":
                            checker.Required("tag");
                            checker.Optional("connectionname");
                            checker.Optional("connectiondisplayname");
                            goal = new Traitor.GoalUnwiring(Config.GetAttributeString("tag", null), Config.GetAttributeString("connectionname", null), Config.GetAttributeString("connectiondisplayname)", null));
                            break;
                        case "transformentity":
                            checker.Required("entities", "entitytypes");
                            checker.Optional("catalystid");
                            goal = new Traitor.GoalEntityTransformation(Config.GetAttributeStringArray("entities", null), Config.GetAttributeStringArray("entitytypes", null), Config.GetAttributeString("catalystid", null));
                            break;
                        case "keeptransformedalive":
                            checker.Required("speciesname");
                            goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeIdentifier("speciesname", Identifier.Empty));
                            break;
                        default:
                            GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error);
                            break;
                    }
                }
                if (goal == null)
                {
                    return null;
                }
                foreach (var element in Config.Elements())
                {
                    switch (element.Name.ToString().ToLowerInvariant())
                    {
                        case "modifier":
                        {
                            using (var checker = new AttributeChecker(element))
                            {
                                checker.Required("type");
                                var modifierType = element.GetAttributeString("type", "");
                                switch (modifierType)
                                {
                                    case "duration":
                                    {
                                        checker.Optional("cumulative", "duration", "infotext");
                                        var isCumulative = element.GetAttributeBool("cumulative", false);
                                        goal = new Traitor.GoalHasDuration(goal, element.GetAttributeFloat("duration", 5.0f), isCumulative, element.GetAttributeString("infotext", isCumulative ? "TraitorGoalWithCumulativeDurationInfoText" : "TraitorGoalWithDurationInfoText"));
                                        break;
                                    }
                                    case "timelimit":
                                        checker.Optional("timelimit", "infotext");
                                        goal = new Traitor.GoalHasTimeLimit(goal, element.GetAttributeFloat("timelimit", 180.0f), element.GetAttributeString("infotext", "TraitorGoalWithTimeLimitInfoText"));
                                        break;
                                    case "optional":
                                        checker.Optional("infotext");
                                        goal = new Traitor.GoalIsOptional(goal, element.GetAttributeString("infotext", "TraitorGoalIsOptionalInfoText"));
                                        break;
                                    default:
                                        GameServer.Log($"Unrecognized modifier type \"{modifierType}\".", ServerLog.MessageType.Error);
                                        break;
                                }
                            }
                            break;
                        }
                    }
                }
                foreach (var element in Config.Elements())
                {
                    var elementName = element.Name.ToString().ToLowerInvariant();
                    switch (elementName)
                    {
                        case "modifier":
                            // loaded above
                            break;
                        case "infotext":
                        {
                            using (var checker = new AttributeChecker(element))
                            {
                                checker.Required("id");
                                var id = element.GetAttributeString("id", null);
                                if (id != null)
                                {
                                    goal.InfoTextId = id;
                                }
                            }
                            break;
                        }
                        case "completedtext":
                        {
                            using (var checker = new AttributeChecker(element))
                            {
                                checker.Required("id");
                                var id = element.GetAttributeString("id", null);
                                if (id != null)
                                {
                                    goal.CompletedTextId = id;
                                }
                            }
                            break;
                        }
                        default:
                            GameServer.Log($"Unrecognized element \"{element.Name}\" in goal.", ServerLog.MessageType.Error);
                            break;
                    }
                }
                return goal;
            }
        }


        public abstract class ObjectiveBase
        {
            public HashSet<string> Roles { get; } = new HashSet<string>();

            public abstract void InstantiateGoals();
            public abstract Traitor.Objective Instantiate(IEnumerable<string> roles);
        }

        protected class Objective : ObjectiveBase
        {
            public string InfoText { get; internal set; }
            public string StartMessageTextId { get; internal set; }
            public string StartMessageServerTextId { get; internal set; }
            public string EndMessageSuccessTextId { get; internal set; }
            public string EndMessageSuccessDeadTextId { get; internal set; }
            public string EndMessageSuccessDetainedTextId { get; internal set; }
            public string EndMessageFailureTextId { get; internal set; }
            public string EndMessageFailureDeadTextId { get; internal set; }
            public string EndMessageFailureDetainedTextId { get; internal set; }
            public int ShuffleGoalsCount { get; internal set; }

            public readonly List<Goal> Goals = new List<Goal>();

            private List<Traitor.Goal> goalInstances = null;

            public override void InstantiateGoals()
            {
                goalInstances = Goals.ConvertAll(goal =>
                {
                    var instance = goal.Instantiate();
                    if (instance == null)
                    {
                        GameServer.Log($"Failed to instantiate goal \"{goal.Type}\".", ServerLog.MessageType.Error);
                    }
                    return instance;
                }).FindAll(goal => goal != null);
            }

            public override Traitor.Objective Instantiate(IEnumerable<string> roles)
            {
                var result = new Traitor.Objective(InfoText, ShuffleGoalsCount, roles.ToArray(), goalInstances);
                if (StartMessageTextId != null)
                {
                    result.StartMessageTextId = StartMessageTextId;
                }
                if (StartMessageServerTextId != null)
                {
                    result.StartMessageServerTextId = StartMessageServerTextId;
                }
                if (EndMessageSuccessTextId != null)
                {
                    result.EndMessageSuccessTextId = EndMessageSuccessTextId;
                }
                if (EndMessageSuccessDeadTextId != null)
                {
                    result.EndMessageSuccessDeadTextId = EndMessageSuccessDeadTextId;
                }
                if (EndMessageSuccessDetainedTextId != null)
                {
                    result.EndMessageSuccessDetainedTextId = EndMessageSuccessDetainedTextId;
                }
                if (EndMessageFailureTextId != null)
                {
                    result.EndMessageFailureTextId = EndMessageFailureTextId;
                }
                if (EndMessageFailureDeadTextId != null)
                {
                    result.EndMessageFailureDeadTextId = EndMessageFailureDeadTextId;
                }
                if (EndMessageFailureDetainedTextId != null)
                {
                    result.EndMessageFailureDetainedTextId = EndMessageFailureDetainedTextId;
                }
                return result;
            }
        }

        protected class WaitObjective : ObjectiveBase
        {
            private Traitor.GoalWaitForTraitors sharedGoal;

            public override void InstantiateGoals()
            {
                sharedGoal = new Traitor.GoalWaitForTraitors(Roles.Count);
            }

            public override Traitor.Objective Instantiate(IEnumerable<string> roles)
            {
                return new Traitor.Objective("TraitorObjectiveInfoTextWaitForOtherTraitors", -1, roles.ToArray(), new[] { sharedGoal });
            }

            public WaitObjective(ICollection<string> roles)
            {
                Roles.UnionWith(roles);
            }
        }

        public class Role
        {
            public readonly Traitor.TraitorMission.RoleFilter Filter;

            public Role(IEnumerable<Traitor.TraitorMission.RoleFilter> filters)
            {
                Filter = character => filters.All(filter => filter(character));
            }

            public Role()
            {
                Filter = character => true;
            }
        }
        public readonly Dictionary<string, Role> Roles = new Dictionary<string, Role>();

        public readonly Identifier Identifier;
        public readonly string StartText;
        public readonly string EndMessageSuccessText;
        public readonly string EndMessageSuccessDeadText;
        public readonly string EndMessageSuccessDetainedText;
        public readonly string EndMessageFailureText;
        public readonly string EndMessageFailureDeadText;
        public readonly string EndMessageFailureDetainedText;

        public readonly List<ObjectiveBase> Objectives = new List<ObjectiveBase>();

        public Traitor.TraitorMission Instantiate()
        {
            var objectivesWithSync = new List<ObjectiveBase>();
            var objectivesCount = Objectives.Count;
            if (objectivesCount > 0)
            {
                var pendingRoles = new HashSet<string>();
                var pendingCount = 1;
                objectivesWithSync.Add(Objectives[0]);
                pendingRoles.UnionWith(Objectives[0].Roles);
                for (var i = 1; i < objectivesCount; ++i)
                {
                    var objective = Objectives[i];
                    if (pendingRoles.IsSupersetOf(objective.Roles))
                    {
                        if (pendingCount > 1)
                        {
                            objectivesWithSync.Add(new WaitObjective(objective.Roles));
                        }
                        pendingRoles.Clear();
                        pendingCount = 0;
                    }
                    objectivesWithSync.Add(objective);
                    pendingRoles.UnionWith(objective.Roles);
                    ++pendingCount;
                }
                if (pendingCount > 1 && pendingRoles.IsSubsetOf(Roles.Keys))
                {
                    // TODO: If last objective includes only one traitor, other traitors will get the wrong end message.
                    objectivesWithSync.Add(new WaitObjective(Roles.Keys));
                }
            }

            return new Traitor.TraitorMission(
                Identifier,
                StartText ?? "TraitorMissionStartMessage",
                EndMessageSuccessText ?? "TraitorObjectiveEndMessageSuccess",
                EndMessageSuccessDeadText ?? "TraitorObjectiveEndMessageSuccessDead",
                EndMessageSuccessDetainedText ?? "TraitorObjectiveEndMessageSuccessDetained",
                EndMessageFailureText ?? "TraitorObjectiveEndMessageFailure",
                EndMessageFailureDeadText ?? "TraitorObjectiveEndMessageFailureDead",
                EndMessageFailureDetainedText ?? "TraitorObjectiveEndMessageFailureDetained",
                Roles.ToDictionary(kv => kv.Key, kv => kv.Value.Filter),
                objectivesWithSync.SelectMany(objective =>
                {
                    objective.InstantiateGoals();
                    return objective.Roles.Select(role => objective.Instantiate(new[] { role }));
                }).ToArray());
        }

        protected Goal LoadGoal(XElement goalRoot)
        {
            var goalType = goalRoot.GetAttributeString("type", "");
            return new Goal(goalType, goalRoot);
        }

         protected Objective LoadObjective(XElement objectiveRoot, string[] allRoles)
         {
            var allRolesSet = new HashSet<string>(allRoles);
            var result = new Objective
            {
                ShuffleGoalsCount = objectiveRoot.GetAttributeInt("shuffleGoalsCount", -1)
            };
            var objectiveRoles = objectiveRoot.GetAttributeStringArray("roles", allRoles);
            if (!allRolesSet.IsSupersetOf(objectiveRoles))
            {
                var unrecognized = new HashSet<string>(objectiveRoles);
                unrecognized.ExceptWith(allRoles);
                GameServer.Log($"Undefined role(s) \"{string.Join(", ", unrecognized)}\" set for Objective.", ServerLog.MessageType.Error);
            }
            result.Roles.UnionWith(allRolesSet.Intersect(objectiveRoles));

            foreach (var element in objectiveRoot.Elements())
            {
                using (var checker = new AttributeChecker(element))
                {
                    switch (element.Name.ToString().ToLowerInvariant())
                    {
                        case "infotext":
                            checker.Required("id");
                            result.InfoText = element.GetAttributeString("id", null);
                            break;
                        case "startmessage":
                            checker.Required("id");
                            result.StartMessageTextId = element.GetAttributeString("id", null);
                            break;
                        case "startmessageserver":
                            checker.Required("id");
                            result.StartMessageServerTextId = element.GetAttributeString("id", null);
                            break;
                        case "endmessagesuccess":
                            checker.Required("id");
                            result.EndMessageSuccessTextId = element.GetAttributeString("id", null);
                            break;
                        case "endmessagesuccessdead":
                            checker.Required("id");
                            result.EndMessageSuccessDeadTextId = element.GetAttributeString("id", null);
                            break;
                        case "endmessagesuccessdetained":
                            checker.Required("id");
                            result.EndMessageSuccessDetainedTextId = element.GetAttributeString("id", null);
                            break;
                        case "endmessagefailure":
                            checker.Required("id");
                            result.EndMessageFailureTextId = element.GetAttributeString("id", null);
                            break;
                        case "endmessagefailuredead":
                            checker.Required("id");
                            result.EndMessageFailureDeadTextId = element.GetAttributeString("id", null);
                            break;
                        case "endmessagefailuredetained":
                            checker.Required("id");
                            result.EndMessageFailureDetainedTextId = element.GetAttributeString("id", null);
                            break;
                        case "goal":
                        {
                            var goal = LoadGoal(element);
                            if (goal != null)
                            {
                                result.Goals.Add(goal);
                            }
                            break;
                        }
                        default:
                            GameServer.Log($"Unrecognized element \"{element.Name}\" under Objective.", ServerLog.MessageType.Error);
                            break;
                    }
                }
            }
            return result;
        }

        protected Role LoadRole(XElement roleRoot)
        {
            var filters = new List<Traitor.TraitorMission.RoleFilter>();
            var jobs = roleRoot.GetAttributeStringArray("jobs", null);
            if (jobs != null)
            {
                var jobsSet = new HashSet<string>(jobs.Select(job => job.ToLower(CultureInfo.InvariantCulture)));
                filters.Add(character => character.Info?.Job != null && jobsSet.Contains(character.Info.Job.Name.ToLower().Value));
            }
            return new Role(filters);
        }

        public TraitorMissionPrefab(ContentXElement missionRoot)
        {
            Identifier = missionRoot.GetAttributeIdentifier("identifier", Identifier.Empty);
            foreach (var element in missionRoot.Elements())
            {
                using (var checker = new AttributeChecker(element))
                {
                    switch (element.Name.ToString().ToLowerInvariant())
                    {
                        case "role":
                            checker.Required("id");
                            checker.Optional("jobs");
                            Roles.Add(element.GetAttributeString("id", null), LoadRole(element));
                            break;
                    }
                }
            }
            if (!Roles.Any())
            {
                Roles.Add("traitor", new Role());
            }
            foreach (var element in missionRoot.Elements())
            {
                using (var checker = new AttributeChecker(element))
                {
                    switch (element.Name.ToString().ToLowerInvariant())
                    {
                        case "role":
                            // handled above
                            break;
                        case "startinfotext":
                            checker.Required("id");
                            StartText = element.GetAttributeString("id", null);
                            break;
                        case "endmessagesuccess":
                            checker.Required("id");
                            EndMessageSuccessText = element.GetAttributeString("id", null);
                            break;
                        case "endmessagesuccessdead":
                            checker.Required("id");
                            EndMessageSuccessDeadText = element.GetAttributeString("id", null);
                            break;
                        case "endmessagesuccessdetained":
                            checker.Required("id");
                            EndMessageSuccessDetainedText = element.GetAttributeString("id", null);
                            break;
                        case "endmessagefailure":
                            checker.Required("id");
                            EndMessageFailureText = element.GetAttributeString("id", null);
                            break;
                        case "endmessagefailuredead":
                            checker.Required("id");
                            EndMessageFailureDeadText = element.GetAttributeString("id", null);
                            break;
                        case "endmessagefailuredetained":
                            checker.Required("id");
                            EndMessageFailureDetainedText = element.GetAttributeString("id", null);
                            break;
                        case "objective":
                        {
                            var objective = LoadObjective(element, Roles.Keys.ToArray());
                            if (objective != null)
                            {
                                Objectives.Add(objective);
                            }
                            break;
                        }
                        default:
                            GameServer.Log($"Unrecognized element \"{element.Name}\"under TraitorMission.", ServerLog.MessageType.Error);
                            break;
                    }
                }
            }
        }
    }
}
