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.SellExplorationData) ||
                e.Is(Events.SellMicroResources) ||
                e.Is(Events.RedeemVoucher) ||
                e.Is(Events.FactionKillBond) ||
                e.Is(Events.MarketBuy) ||
                e.Is(Events.MarketSell)
                ;
        }

        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
                          ;
            var relevant = from log in entries.SelectMany(array => array)
                           where IsRelevant(log)
                           select log
                           ;

            Dictionary<int, MissionAcceptedEntry> acceptedMissions = new Dictionary<int, MissionAcceptedEntry>();

            Dictionary<string, int> buyCost = new Dictionary<string, int>();

            string current_system = null;
            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;
                } else if (e.Is(Events.FSDJump)) {
                    /* Gleem current system and controlling faction from this message.
                     */
                    current_system = (e as FSDJumpEntry).StarSystem;
                    controlling_faction = (e as FSDJumpEntry).SystemFaction;
                } 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;
                    if (!string.IsNullOrEmpty(location.SystemFaction)) {
                        controlling_faction = location.SystemFaction;
                    }
                    if (!string.IsNullOrEmpty(location.StationName)) {
                        current_station = location.StationName;
                    }
                } else if (e.Is(Events.MissionCompleted)) {
                    var completed = e as MissionCompletedEntry;
                    results.Add(new MissionCompleted(completed) {
                        System = current_system,
                        Station = current_station
                    });
                    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 (string other in completed.AffectedFactions.Where(x => !x.Equals(results[0].Faction))) {
                        string faction = other;
                        string influence = completed.GetInfluenceForFaction(faction);

                        results.Add(new InfluenceSupport() {
                            Faction = faction,
                            Influence = influence,
                            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.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;
                } else if (e.Is(Events.MarketSell)) {
                    MarketSellEntry sell = e as MarketSellEntry;
                    int profit = 0;

                    if (!buyCost.ContainsKey(sell.Type)) {
                        OnLog?.Invoke("Could not find buy order for the given commodity. Please adjust profit manually.");
                    } else {
                        int avg = buyCost[sell.Type];
                        profit = 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) > 0)
                        .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);
        }
    }
}