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;
        }

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

        public void Scan(PlayerJournal journal, DateTime start, DateTime end) {
            var entries = from file in journal.Files
                          where file.NormalisedTimestamp >= start && file.NormalisedTimestamp <= end
                          select file.Entries
                          ;
            Scan(entries.SelectMany(x => x).ToList());
        }

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

            List<Entry> relevant = entries.Where(x => IsRelevant(x)).ToList();
            Dictionary<int, MissionAcceptedEntry> acceptedMissions = new Dictionary<int, MissionAcceptedEntry>();
            Dictionary<string, long> buyCost = new Dictionary<string, long>();
            Dictionary<ulong, string> systems = new Dictionary<ulong, string>();

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

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

            foreach (var 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);
                    }
                } 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;
                    }
                } else if (e.Is(Events.CommitCrime)) {
                    CommitCrimeEntry crime = e as CommitCrimeEntry;

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

                    results.Add(new FoulMurder(crime) {
                        System = current_system,
                        Faction = crime.Faction,
                    });
                    collate = true;
                } else if (e.Is(Events.MissionCompleted)) {
                    var completed = e as MissionCompletedEntry;
                    results.Add(new MissionCompleted(completed) {
                        System = current_system,
                        Station = current_station,
                        SystemAddress = current_system_address,
                    });
                    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)) {
                            continue;
                        }
                        foreach (var influences in other.Value) {
                            ulong system_address = influences.Key;
                            if (!faction.Equals(results[0].Faction) ||
                                (faction.Equals(results[0].Faction) && system_address != current_system_address)) {
                                string system = systems.TryGetValue(system_address, out string sys) ? sys : "";
                                results.Add(new InfluenceSupport() {
                                    Faction = faction,
                                    Influence = influences.Value,
                                    System = system,
                                    SystemAddress = system_address,
                                    RelevantMission = results[0] as MissionCompleted
                                });
                            }
                        }
                    }
                } else if (e.Is(Events.MissionAccepted)) {
                    MissionAcceptedEntry accepted = e as MissionAcceptedEntry;
                    acceptedMissions[accepted.MissionID] = accepted;
                } else if (e.Is(Events.MissionFailed)) {
                    var failed = e as MissionFailedEntry;
                    MissionAcceptedEntry accepted = null;
                    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;
                    }
                    results.Add(new MissionFailed(accepted) { Failed = failed, System = current_system });
                    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)) {
                    /* Same for selling combat vouchers. Only the current controlling faction matters here.
                     */
                    results.Add(new Vouchers(e as RedeemVoucherEntry) {
                        System = current_system,
                        Station = current_station,
                        Faction = (e as RedeemVoucherEntry).Factions.FirstOrDefault() ?? "",
                        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);
        }
    }
}