using System;
using System.Collections.Generic;
using System.Linq;
using EDJournal;

namespace EliteBGS.BGS {
    public class Report {
        private List<Objective> objectives = new List<Objective>();

        public delegate void OnLogDelegate(string log);

        public event OnLogDelegate OnLog;

        public List<Objective> Objectives {
            get { return objectives; }
            set { objectives = value; }
        }

        public bool AddObjective(Objective objective) {
            var found = objectives.Find(x => x.CompareTo(objective) == 0);
            bool added = false;

            if (found == null) {
                objectives.Add(objective);
                added = true;
            }

            return added;
        }

        public static bool IsRelevant(Entry e) {
            return e.Is(Events.CommitCrime) ||
                e.Is(Events.Docked) ||
                e.Is(Events.FactionKillBond) ||
                e.Is(Events.FSDJump) ||
                e.Is(Events.Location) ||
                e.Is(Events.MarketBuy) ||
                e.Is(Events.MarketSell) ||
                e.Is(Events.MissionAccepted) ||
                e.Is(Events.MissionFailed) ||
                e.Is(Events.MultiSellExplorationData) ||
                e.Is(Events.RedeemVoucher) ||
                e.Is(Events.SearchAndRescue) ||
                e.Is(Events.SellExplorationData) ||
                e.Is(Events.SellMicroResources) ||
                e.Is(Events.SellOrganicData) ||
                e.Is(Events.ShipTargeted) ||
                e.Is(Events.MissionCompleted)
                ;
        }

        public void Scan(PlayerJournal journal, DateTime start, DateTime end) {
            /* Log files only get rotated if you restart the game client. This means that there might
             * be - say - entries from the 4th of May in the file with a timestamp of 3rd of May. This
             * happens if you happen to play a session late into the night.
             * At first I tried extracting the first and last line of a file to see the date range, but
             * if you have a lot of files this becomes quite slow, and quite the memory hog (as journal
             * files have to be read in their entirety to check this). So we assume that you can't play
             * three days straight, and keep the code fast.
             */
            DateTime actualstart = start.AddDays(-3);
            List<Entry> entries = journal.Files
                .Where(f => f.NormalisedDateTime >= actualstart && f.NormalisedDateTime <= end)
                .SelectMany(e => e.Entries)
                .ToList()
                ;
            // Now further sort the list down to entries that are actually within the given datetime
            // Note that entry datetimes are not normalised, so we have to sort until end + 1 day
            DateTime actualend = end.AddDays(1);

            entries = entries
                .Where(e => e.Timestamp >= start && e.Timestamp < actualend)
                .ToList()
                ;
            Scan(entries);
        }

        public void Scan(List<Entry> entries) {
            if (entries.Count <= 0) {
                return;
            }

            List<Entry> relevant = entries
                .Where(x => IsRelevant(x))
                .ToList()
                ;

            Dictionary<ulong, MissionAcceptedEntry> acceptedMissions = new Dictionary<ulong, MissionAcceptedEntry>();
            Dictionary<string, long> buyCost = new Dictionary<string, long>();
            Dictionary<ulong, string> systems = new Dictionary<ulong, string>();
            Dictionary<string, string> npcfactions = new Dictionary<string, string>();
            Dictionary<string, List<Faction>> system_factions = new Dictionary<string, List<Faction>>();

            // A dictionary resolving to a station at which each mission was accepted
            Dictionary<ulong, string> acceptedStations = new Dictionary<ulong, string>();
            // A dictionary resolving to a system at which each mission was accepted
            Dictionary<ulong, ulong> acceptedSystems = new Dictionary<ulong, ulong>();

            string current_system = null;
            ulong current_system_address = 0;
            string current_station = null;
            string controlling_faction = null;

            objectives.ForEach(x => x.Clear());

            foreach (Entry e in relevant) {
                List<LogEntry> results = new List<LogEntry>();
                bool collate = false;

                if (e.Is(Events.Docked)) {
                    DockedEntry docked = e as DockedEntry;
                    /* gleem the current station from this message
                     */
                    current_station = docked.StationName;
                    current_system = docked.StarSystem;
                    controlling_faction = docked.StationFaction;
                    current_system_address = docked.SystemAddress;

                    if (!systems.ContainsKey(docked.SystemAddress)) {
                        systems.Add(docked.SystemAddress, docked.StarSystem);
                    }
                } else if (e.Is(Events.FSDJump)) {
                    /* Gleem current system and controlling faction from this message.
                     */
                    FSDJumpEntry fsd = e as FSDJumpEntry;
                    current_system_address = fsd.SystemAddress;
                    current_system = fsd.StarSystem;
                    controlling_faction = fsd.SystemFaction;

                    if (!systems.ContainsKey(fsd.SystemAddress)) {
                        systems.Add(fsd.SystemAddress, fsd.StarSystem);
                    }

                    if (!system_factions.ContainsKey(fsd.StarSystem) &&
                        fsd.SystemFactions.Count > 0) {
                        system_factions[fsd.StarSystem] = fsd.SystemFactions;
                    }
                } else if (e.Is(Events.Location)) {
                    /* Get current system, faction name and station from Location message
                     */
                    LocationEntry location = e as LocationEntry;

                    current_system = location.StarSystem;
                    current_system_address = location.SystemAddress;

                    if (!systems.ContainsKey(location.SystemAddress)) {
                        systems.Add(location.SystemAddress, location.StarSystem);
                    }

                    if (!string.IsNullOrEmpty(location.SystemFaction)) {
                        controlling_faction = location.SystemFaction;
                    }
                    if (!string.IsNullOrEmpty(location.StationName)) {
                        current_station = location.StationName;
                    }

                    if (!system_factions.ContainsKey(location.StarSystem) &&
                        location.SystemFactions.Count > 0) {
                        system_factions[location.StarSystem] = location.SystemFactions;
                    }
                } else if (e.Is(Events.ShipTargeted)) {
                    ShipTargetedEntry targeted = e as ShipTargetedEntry;

                    if (string.IsNullOrEmpty(targeted.PilotNameLocalised) ||
                        string.IsNullOrEmpty(targeted.Faction)) {
                        continue;
                    }

                    npcfactions[targeted.PilotNameLocalised] = targeted.Faction;
                } else if (e.Is(Events.CommitCrime)) {
                    CommitCrimeEntry crime = e as CommitCrimeEntry;
                    string faction = crime.Faction;

                    if (!crime.IsMurder) {
                        /* we don't care about anything but murder for now */
                        continue;
                    }

                    /* use localised victim name if we have it, otherwise use normal name */
                    string victim = crime.VictimLocalised;
                    if (string.IsNullOrEmpty(victim)) {
                        victim = crime.Victim;
                    }

                    if (!npcfactions.ContainsKey(victim)) {
                        /* The faction in the crime report is the faction that issues the bounty,
                         * and not the faction of the victim.
                         */
                        OnLog?.Invoke(string.Format(
                            "No faction found for victim \"{0}\", using faction that issued the bounty instead.",
                            victim, crime.Faction
                            ));
                    } else {
                        faction = npcfactions[victim];
                    }

                    results.Add(new FoulMurder(crime) {
                        System = current_system,
                        Faction = faction,
                    });
                    collate = true;
                } else if (e.Is(Events.MissionCompleted)) {
                    MissionCompletedEntry completed = e as MissionCompletedEntry;
                    MissionAcceptedEntry accepted = null;
                    MissionCompleted main_mission = null;
                    ulong accepted_address;
                    string accepted_system;

                    string target_faction_name = completed.TargetFaction;
                    string source_faction_name = completed.Faction;

                    if (!acceptedMissions.TryGetValue(completed.MissionID, out accepted)) {
                        OnLog?.Invoke(string.Format(
                            "Unable to find mission acceptance for mission \"{0}\". " +
                            "Please extend range to include the mission acceptance.", completed.HumanReadableName
                            ));
                        continue;
                    }

                    if (!acceptedSystems.TryGetValue(completed.MissionID, out accepted_address)) {
                        OnLog?.Invoke(string.Format(
                            "Unable to figure out in which system mission \"{0}\" was accepted.", completed.HumanReadableName
                            ));
                        continue;
                    }

                    if (!systems.TryGetValue(accepted_address, out accepted_system)) {
                        OnLog?.Invoke(string.Format(
                            "Unable to figure out in which system mission \"{0}\" was accepted.", completed.HumanReadableName
                            ));
                        continue;
                    }

                    if (completed.HumanReadableNameWasGenerated) {
                        /* If the human readable name was generated, we send a log message. Because the
                         * generated names all sort of suck, we should have more human readable names in
                         * in the lookup dictionary.
                         */
                        OnLog?.Invoke("Human readable name for mission \"" +
                            completed.Name +
                            "\" was generated, please report this.");
                    }

                    foreach (var other in completed.Influences) {
                        string faction = other.Key;
                        if (string.IsNullOrEmpty(faction)) {
                            OnLog?.Invoke(string.Format(
                                "Mission \"{0}\" has empty faction name in influence block, "+
                                "so this influence support was ignored. " +
                                "Please check the README on why this happens.", completed.HumanReadableName)
                                );
                            continue;
                        }

                        /* Now comes the fun part. Sometimes the influence list is empty for a faction.
                         * This happens if the faction in question 
                         */
                        if (other.Value.Count() == 0) {
                            OnLog?.Invoke(string.Format(
                                "Mission \"{0}\" gave no influence to \"{1}\", so we assume this is because the " +
                                "faction is in a conflict and cannot gain influence right now. " +
                                "If this assessment is wrong, just remove the entry from the objective list.",
                                completed.HumanReadableName, faction
                                ));

                            if (string.Compare(target_faction_name, faction, true) == 0) {
                                /* here we assume that if the faction in question is the target faction,
                                 * that we gave said target faction no influence in the target system, aka
                                 * current system
                                 */
                                other.Value.Add(current_system_address, "");
                                OnLog?.Invoke(string.Format(
                                    "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the target faction " +
                                    "of the mission, we assume the influence was gained in \"{2}\". " +
                                    "Please remove the entry if this assumption is wrong.",
                                    completed.HumanReadableName, faction, current_system
                                    ));
                            } else if (string.Compare(source_faction_name, faction, true) == 0) {
                                /* source faction of the mission is not getting any influence. This could be because
                                 * the source faction is in an election state in its home system and cannot gain any
                                 * influence. It may also very well be that the source and target faction are the same
                                 * since the faction is present in both target and source system. In which case we add
                                 * both and hope for the best.
                                 */
                                other.Value.Add(accepted_address, "");
                                OnLog?.Invoke(string.Format(
                                    "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the source faction " +
                                    "of the mission, we assume the influence was gained in \"{2}\". " +
                                    "Please remove the entry if this assumption is wrong.",
                                    completed.HumanReadableName, faction, accepted_system
                                    ));

                                /* check if source/target faction are equal, in which case we also need an entry
                                 * for the target system. As said factions can be present in two systems, and can
                                 * give missions that target each other.
                                 */
                                if (string.Compare(source_faction_name, target_faction_name, true) == 0) {
                                    other.Value.Add(current_system_address, "");
                                    OnLog?.Invoke(string.Format(
                                        "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the source and target faction " +
                                        "of the mission, we assume the influence was also gained in target system \"{2}\". " +
                                        "Please remove the entry if this assumption is wrong.",
                                        completed.HumanReadableName, faction, current_system
                                        ));
                                }
                            }
                        }

                        foreach (var influences in other.Value) {
                            ulong system_address = influences.Key;
                            string system, accepted_station;

                            if (!systems.TryGetValue(system_address, out system)) {
                                OnLog?.Invoke(string.Format(
                                    "Unknown system \"{0}\" unable to assign that mission a target.", system_address
                                    ));
                                continue;
                            }

                            if (!acceptedStations.TryGetValue(completed.MissionID, out accepted_station)) {
                                OnLog?.Invoke(string.Format(
                                    "Unable to figure out in which station mission \"{0}\" was accepted.", completed.HumanReadableName
                                    ));
                                continue;
                            }

                            if (faction.Equals(source_faction_name) && system_address == accepted_address) {
                                /* This is the influence block for the origin of the mission.
                                 */
                                main_mission = new MissionCompleted(completed) {
                                    System = accepted_system,
                                    Faction = source_faction_name,
                                    SystemAddress = accepted_address,
                                    Station = accepted_station,
                                };
                                results.Add(main_mission);
                            } else if (!faction.Equals(source_faction_name) ||
                                (faction.Equals(source_faction_name) && system_address != accepted_address)) {
                                /* This block is for secondary factions (first if), or if the secondary faction
                                 * is the same as the mission giver, but in another system (second if).
                                 */
                                results.Add(new InfluenceSupport() {
                                    Faction = faction,
                                    Influence = influences.Value,
                                    System = system,
                                    SystemAddress = system_address,
                                    RelevantMission = completed
                                });
                            }
                        }
                    }
                } else if (e.Is(Events.MissionAccepted)) {
                    MissionAcceptedEntry accepted = e as MissionAcceptedEntry;
                    ulong id = accepted.MissionID;
                    if (!acceptedMissions.ContainsKey(id)) {
                        acceptedMissions[id] = accepted;
                    }
                    if (!acceptedStations.ContainsKey(id)) {
                        acceptedStations[id] = current_station;
                    }
                    if (!acceptedSystems.ContainsKey(id)) {
                        acceptedSystems[id] = current_system_address;
                    }
                } else if (e.Is(Events.MissionFailed)) {
                    MissionFailedEntry failed = e as MissionFailedEntry;
                    MissionAcceptedEntry accepted = null;
                    ulong accepted_address = 0;
                    string accepted_system;
                    string accepted_station;

                    if (!acceptedMissions.TryGetValue(failed.MissionID, out accepted)) {
                        OnLog?.Invoke("A mission failed which wasn't accepted in the given time frame. " +
                            "Please adjust start date to when the mission was accepted to include it in the list.");
                        continue;
                    }

                    if (!acceptedSystems.TryGetValue(failed.MissionID, out accepted_address)) {
                        OnLog?.Invoke(string.Format(
                            "Unable to figure out in which system mission \"{0}\" was accepted.", accepted.Name
                            ));
                        continue;
                    }

                    if (!systems.TryGetValue(accepted_address, out accepted_system)) {
                        OnLog?.Invoke(string.Format(
                            "Unable to figure out in which system mission \"{0}\" was accepted.", accepted.Name
                            ));
                        continue;
                    }

                    if (!acceptedStations.TryGetValue(failed.MissionID, out accepted_station)) {
                        OnLog?.Invoke(string.Format(
                            "Unable to figure out in which station mission \"{0}\" was accepted.", accepted.Name
                            ));
                        continue;
                    }

                    results.Add(new MissionFailed(accepted) {
                        Failed = failed, 
                        System = accepted_system,
                        Station = accepted_station,
                        Faction = accepted.Faction,
                        SystemAddress = accepted_address,
                    });

                    if (failed.HumanReadableName == null) {
                        OnLog?.Invoke("Human readable name for mission \"" +
                            failed.Name +
                            "\" was not recognised");
                    }

                    /* Mission failed should be collated if they are in the same system/station
                     */
                    collate = true;
                } else if (e.Is(Events.SellExplorationData)) {
                    results.Add(new Cartographics(e as SellExplorationDataEntry) {
                        System = current_system,
                        Station = current_station,
                        Faction = controlling_faction,
                    });

                    /* colate single cartographic selling into one */
                    collate = true;
                } else if (e.Is(Events.SellOrganicData)) {
                    /* organic data sold to Vista Genomics */
                    results.Add(new OrganicData(e as SellOrganicDataEntry) {
                        System = current_system,
                        Station = current_station,
                        Faction = controlling_faction,
                    });

                    collate = true;
                } else if (e.Is(Events.MultiSellExplorationData)) {
                    /* For multi-sell-exploraton-data only the controlling faction of the station sold to matters.
                     */
                    results.Add(new Cartographics(e as MultiSellExplorationDataEntry) {
                        System = current_system,
                        Station = current_station,
                        Faction = controlling_faction
                    });

                    collate = true;
                } else if (e.Is(Events.RedeemVoucher)) {
                    RedeemVoucherEntry voucher = e as RedeemVoucherEntry;
                    List<Faction> current_factions = new List<Faction>();

                    if (system_factions.ContainsKey(current_system)) {
                        current_factions = system_factions[current_system];
                    } else {
                        OnLog?.Invoke("There are no current system factions, so turned in vouchers were ignored.");
                        continue;
                    }

                    foreach (string faction in voucher.Factions) {
                        if (current_factions.Find(x => x.Name == faction) == null) {
                            OnLog?.Invoke(
                                string.Format("Vouchers for \"{0}\" were ignored in \"{1}\" since said " +
                                "faction is not present there.", faction, current_system)
                                );
                            continue; /* faction is not present, so it is ignored */
                        }

                        /* Same for selling combat vouchers. Only the current controlling faction matters here.
                         */
                        results.Add(new Vouchers(voucher) {
                            System = current_system,
                            Station = current_station,
                            Faction = faction,
                            ControllingFaction = controlling_faction,
                        });
                    }

                    collate = true;
                } else if (e.Is(Events.SellMicroResources)) {
                    results.Add(new SellMicroResources(e as SellMicroResourcesEntry) {
                        Faction = controlling_faction,
                        Station = current_station,
                        System = current_system
                    });
                } else if (e.Is(Events.MarketBuy)) {
                    MarketBuyEntry buy = e as MarketBuyEntry;
                    if (string.IsNullOrEmpty(buy.Type) || buy.BuyPrice == 0) {
                        continue;
                    }
                    buyCost[buy.Type] = buy.BuyPrice;

                    results.Add(new BuyCargo(buy) {
                        Faction = controlling_faction,
                        Station = current_station,
                        System = current_system,
                    });

                    collate = true;
                } else if (e.Is(Events.SearchAndRescue)) {
                    results.Add(new SearchAndRescue(e as SearchAndRescueEntry) {
                        Faction = controlling_faction,
                        Station = current_station,
                        System = current_system,
                    });

                    collate = true;
                } else if (e.Is(Events.MarketSell)) {
                    MarketSellEntry sell = e as MarketSellEntry;
                    long profit = 0;

                    if (!buyCost.ContainsKey(sell.Type)) {
                        OnLog?.Invoke("Could not find buy order for the given commodity. Please adjust profit manually.");
                    } else {
                        long avg = buyCost[sell.Type];
                        profit = (long)sell.TotalSale - (avg * sell.Count);
                    }

                    results.Add(new SellCargo(e as MarketSellEntry) {
                        Faction = controlling_faction,
                        Station = current_station,
                        System = current_system,
                        Profit = profit
                    });
                }

                if (results == null || results.Count <= 0) {
                    continue;
                }

                foreach (LogEntry entry in results) {
                    /* Find all objectives that generally match.
                     */
                    var matches = objectives
                        .Where(x => x.Matches(entry) >= 2)
                        .OrderBy(x => x.Matches(entry))
                        ;

                    Objective objective = null;
                    if (matches != null && matches.Count() > 0) {
                        /* Then select the one that matches the most.
                         */
                        objective = matches
                            .OrderBy(x => x.Matches(entry))
                            .Reverse()
                            .First()
                            ;
                    } else {
                        /* create a new objective if we don't have one */
                        objective = new Objective() {
                            Station = entry.Station,
                            Faction = entry.Faction,
                            System = entry.System,
                        };
                        objectives.Add(objective);
                    }

                    LogEntry existing = null;

                    existing = objective.LogEntries.Find(x => {
                        try {
                            return x.CompareTo(entry) == 0;
                        } catch (NotImplementedException) {
                            return false;
                        }
                    });

                    if (collate && existing != null) {
                        existing.Entries.Add(e);
                    } else if (!collate || existing == null) {
                        objective.LogEntries.Add(entry);
                    }
                }
            }
        }

        public void Scan(PlayerJournal journal) {
            Scan(journal, DateTime.Now, DateTime.Now);
        }
    }
}