using EDPlayerJournal;
using EDPlayerJournal.Entries;

namespace EDPlayerJournal.BGS; 

internal class TransactionParserContext {
    public string? CurrentSystem { get; set; }
    public ulong? CurrentSystemAddress { get; set; }
    public string? CurrentStation { get; set; }
    public string? ControllingFaction { get; set; }

    public bool IsOnFoot { get; set; } = false;

    public string? LastRecordedAwardingFaction { get; set; }

    public ulong? HighestCombatBond { get; set; }

    public bool HaveSeenCapShip { get; set; } = false;
    public bool HaveSeenCaptain { get; set; } = false;
    public bool HaveSeenSpecOps { get; set; } = false;
    public bool HaveSeenCorrespondent { get; set; } = false;

    /// <summary>
    /// Returns true if the current session is legacy
    /// </summary>
    public bool IsLegacy { get; set; } = false;

    /// <summary>
    /// How many on foot kills were done.
    /// </summary>
    public ulong OnFootKills { get; set; } = 0;

    /// <summary>
    /// How many ship kills were done.
    /// </summary>
    public ulong ShipKills { get; set; } = 0;

    /// <summary>
    /// Thargoid scouts killed
    /// </summary>
    public ulong ThargoidScoutKills { get; set; } = 0;

    /// <summary>
    /// Thargoid interceptor kills
    /// </summary>
    public ulong ThargoidInterceptorKills { get; set; } = 0;

    /// <summary>
    /// Whether we have seen an AX warzone NPC talk to us with ReceiveText
    /// </summary>
    public bool HaveSeenAXWarzoneNPC { get; set; } = false;

    /// <summary>
    /// A list of accepted missions index by their mission ID
    /// </summary>
    public Dictionary<ulong, Mission> AcceptedMissions { get; } = new();
    public Dictionary<ulong, Location> AcceptedMissionLocation { get; } = new();
    /// <summary>
    /// A way to lookup a system by its system id
    /// </summary>
    public Dictionary<ulong, string> SystemsByID { get; } = new();
    /// <summary>
    /// A list of factions present in the given star system
    /// </summary>
    public Dictionary<string, List<Faction>> SystemFactions { get; } = new();
    /// <summary>
    /// To which faction a given named NPC belonged to.
    /// </summary>
    public Dictionary<string, string> NPCFaction { get; } = new();
    /// <summary>
    /// Buy costs for a given commodity
    /// </summary>
    public Dictionary<string, long> BuyCost = new();

    public void DiscernCombatZone(TransactionList transactions, Entry e) {
        string? grade = CombatZones.DifficultyLow;
        string cztype;
        ulong highest = HighestCombatBond ?? 0;
        string? faction = LastRecordedAwardingFaction;

        if (HighestCombatBond == null &&
            LastRecordedAwardingFaction == null &&
            HaveSeenAXWarzoneNPC == false) {
            return;
        }

        if (OnFootKills > 0 || IsOnFoot == true) {
            cztype = CombatZones.GroundCombatZone;
            // High on foot combat zones have enforcers that bring 80k a pop
            if (highest >= 60000) {
                grade = CombatZones.DifficultyHigh;
            } else if (highest >= 30000) {
                // In medium conflict zones, the enforcers are worth 30k
                grade = CombatZones.DifficultyMedium;
            } else {
                grade = CombatZones.DifficultyLow;
            }
        } else if (ShipKills > 0 && !IsOnFoot) {
            // Ship combat zones can be identified by the amount of kills
            if (ShipKills > 20) {
                grade = CombatZones.DifficultyHigh;
            } else if (ShipKills > 10) {
                grade = CombatZones.DifficultyMedium;
            }

            // Cap ship, means a high conflict zone
            if (HaveSeenCapShip) {
                grade = CombatZones.DifficultyHigh;
            } else {
                int warzoneNpcs = new List<bool>() { HaveSeenCaptain, HaveSeenCorrespondent, HaveSeenSpecOps }
                                    .Where(x => x == true)
                                    .Count()
                                    ;

                if (warzoneNpcs >= 1 && grade == CombatZones.DifficultyLow) {
                    grade = CombatZones.DifficultyMedium;
                }
            }
            cztype = CombatZones.ShipCombatZone;
        } else if ((ThargoidScoutKills > 0 && ThargoidInterceptorKills > 0) ||
                   HaveSeenAXWarzoneNPC == true) {
            // Could be a thargoid combat zones if interceptors and scouts are there
            cztype = CombatZones.AXCombatZone;
            // Still unknown
            grade = null;
        } else {
            transactions.AddIncomplete(new CombatZone(), "Failed to discern combat zone type", e);
            return;
        }

        CombatZone zone = new CombatZone() {
            System = CurrentSystem,
            Faction = faction,
            IsLegacy = IsLegacy,
            Grade = grade,
            Type = cztype,
            // Sad truth is, if HaveSeenXXX is false, we just don't know for certain
            CapitalShip = HaveSeenCapShip ? true : null,
            SpecOps = HaveSeenSpecOps ? true : null,
            Correspondent = HaveSeenCorrespondent ? true : null,
            Captain = HaveSeenCaptain ? true : null,
        };
        zone.Entries.Add(e);
        transactions.Add(zone);
    }

    public void RecordCombatBond(FactionKillBondEntry e) {
        if (HighestCombatBond == null || e.Reward > HighestCombatBond) {
            HighestCombatBond = e.Reward;
        }

        LastRecordedAwardingFaction = e.AwardingFaction;

        if (IsOnFoot) {
            ++OnFootKills;
        } else {
            ++ShipKills;
        }
    }

    public void ResetCombatZone() {
        HighestCombatBond = null;
        HaveSeenCapShip = false;
        HaveSeenCaptain = false;
        HaveSeenCorrespondent = false;
        HaveSeenSpecOps = false;
        LastRecordedAwardingFaction = null;
        OnFootKills = 0;
        ShipKills = 0;
        ThargoidInterceptorKills = 0;
        ThargoidScoutKills = 0;
        HaveSeenAXWarzoneNPC = false;
    }

    public void BoughtCargo(string? cargo, long? cost) {
        if (cargo == null || cost == null) {
            return;
        }

        BuyCost[cargo] = cost.Value;
    }

    public List<Faction>? GetFactions(string? system) {
        if (system == null || !SystemFactions.ContainsKey(system)) {
            return null;
        }

        return SystemFactions[system];
    }

    public void MissionAccepted(MissionAcceptedEntry? entry) {
        if (entry == null) {
            return;
        }

        MissionAccepted(entry.Mission);
    }

    public void MissionAccepted(Mission? mission) {
        if (CurrentSystem == null || CurrentSystemAddress == null) {
            throw new Exception("Mission accepted without knowing where.");
        }

        if (mission == null) {
            throw new Exception("Mission is null");
        }

        AcceptedMissions.TryAdd(mission.MissionID, mission);

        Location location = new() {
            StarSystem = CurrentSystem,
            SystemAddress = CurrentSystemAddress.Value,
            Station = (CurrentStation ?? ""),
        };

        AcceptedMissionLocation.TryAdd(mission.MissionID, location);
    }
}

public class TransactionList : List<Transaction> {
    public void AddIncomplete(Transaction underlying, string reason, Entry entry) {
        Add(new IncompleteTransaction(underlying, reason, entry));
    }
}

internal interface TransactionParserPart{
    /// <summary>
    /// Parse a given entry of the entry type specified when declaring to implement this
    /// interface. You must add your transaction to the passed list in case you did have
    /// enough information to parse one or more. You may update the parser context
    /// with new information in case the entry yielded new information.
    /// Throw an exception if there was an error or a malformed entry. If you have an
    /// incomplete entry, i.e. not enough information to complete one, add an
    /// IncompleteTransaction to the list
    /// </summary>
    /// <param name="entry">The entry to parse</param>
    /// <param name="context">Parsing context that may contain useful information</param>
    /// <param name="transactions">List of parsed transactions</param>
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions);
}

/// <summary>
/// The location parser only updates the context with useful information, and does not
/// by itself generate any transactions. Location is the best information gatherer here
/// as we are getting controlling faction, system factions, address and station name.
/// </summary>
internal class LocationParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        LocationEntry? entry = e as LocationEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (entry.StarSystem == null) {
            throw new InvalidJournalEntryException();
        }

        context.CurrentSystem = entry.StarSystem;
        context.CurrentSystemAddress = entry.SystemAddress;

        context.SystemsByID.TryAdd(entry.SystemAddress, entry.StarSystem);

        if (!string.IsNullOrEmpty(entry.SystemFaction)) {
            context.ControllingFaction = entry.SystemFaction;
        }

        if (!string.IsNullOrEmpty(entry.StationName)) {
            context.CurrentStation = entry.StationName;
        }

        if (!context.SystemFactions.ContainsKey(entry.StarSystem) &&
            entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
            context.SystemFactions[entry.StarSystem] = entry.SystemFactions;
        }
    }
}

internal class FSDJumpParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        FSDJumpEntry? entry = e as FSDJumpEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (entry.StarSystem == null) {
            throw new InvalidJournalEntryException();
        }

        // If you FSD jump straight out of the combat zone into a different system
        // then the combat zone will be placed in the wrong system otherwise.
        // This call needs to be *before* changing the current system.
        context.DiscernCombatZone(transactions, e);
        context.ResetCombatZone();

        context.CurrentSystem = entry.StarSystem;
        context.CurrentSystemAddress = entry.SystemAddress;

        context.SystemsByID.TryAdd(entry.SystemAddress, entry.StarSystem);

        if (!string.IsNullOrEmpty(entry.SystemFaction)) {
            context.ControllingFaction = entry.SystemFaction;
        }

        if (!context.SystemFactions.ContainsKey(entry.StarSystem) &&
            entry.SystemFactions != null && entry.SystemFactions.Count > 0) {
            context.SystemFactions[entry.StarSystem] = entry.SystemFactions;
        }
    }
}

internal class DockedParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        DockedEntry? entry = e as DockedEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (entry.StarSystem == null || entry.SystemAddress == null) {
            throw new InvalidJournalEntryException();
        }

        context.CurrentSystem = entry.StarSystem;
        context.CurrentSystemAddress = entry.SystemAddress;

        context.SystemsByID.TryAdd(entry.SystemAddress.Value, entry.StarSystem);

        if (!string.IsNullOrEmpty(entry.StationFaction)) {
            context.ControllingFaction = entry.StationFaction;
        }

        if (!string.IsNullOrEmpty(entry.StationName)) {
            context.CurrentStation = entry.StationName;
        }
    }
}

/// <summary>
/// With ship targeted we might find out to which faction a given NPC belonged. This is
/// useful later when said NPC gets killed or murdered.
/// </summary>
internal class ShipTargetedParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        ShipTargetedEntry? entry = e as ShipTargetedEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        // Scan happens in stages, and sometimes this information is not known
        // yet. Do now throw an error, this is expected behaviour.
        if (!string.IsNullOrEmpty(entry.PilotNameLocalised) &&
            !string.IsNullOrEmpty(entry.Faction)) {
            context.NPCFaction.TryAdd(entry.PilotNameLocalised, entry.Faction);
        }

        // We have seen a captain?
        if (NPCs.IsWarzoneCaptain(entry.PilotName)) {
            context.HaveSeenCaptain = true;
        }

        // Spec ops?
        if (NPCs.IsSpecOps(entry.PilotName)) {
            context.HaveSeenSpecOps = true;
        }

        // Correspondent?
        if (NPCs.IsWarzoneCorrespondent(entry.PilotName)) {
            context.HaveSeenCorrespondent = true;
        }
    }
}

/// <summary>
/// Commit crime can result in a transaction, especially if the crime committed is
/// murder.
/// </summary>
internal class CommitCrimeParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        CommitCrimeEntry? entry = e as CommitCrimeEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        // Right now we only care for murder
        if (!entry.IsMurder) {
            return;
        }

        string? victim = entry.Victim;

        if (victim == null) {
            victim = entry.VictimLocalised;
        }

        // If they were not properly scanned prior to the murder we do not have
        // This information. But in the end the name of the NPC does not matter.
        if (victim == null) {
            victim = "Unknown";
        }

        string faction;

        if (entry.IsCrime(CrimeTypes.OnFootMurder)) {
            if (entry.Faction == null) {
                transactions.AddIncomplete(
                    new FoulMurder(),
                    "On foot murder victim did not have a faction", e
                    );
                return;
            }
            // On foot murders are different, there the faction is
            // the faction the NPC belonged too
            faction = entry.Faction;
        } else {
            if (!context.NPCFaction.ContainsKey(victim)) {
                transactions.AddIncomplete(
                    new FoulMurder(),
                    "Crime victim was not properly scanned.", e
                    );
                return;
            }
            faction = context.NPCFaction[victim];
        }

        transactions.Add(new FoulMurder(entry) {
            System = context.CurrentSystem,
            IsLegacy = context.IsLegacy,
            Faction = faction,
        });
    }
}

internal class MissionsParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        MissionsEntry? missions = entry as MissionsEntry;

        if (missions == null) {
            return;
        }

        if (context.CurrentSystem == null || context.CurrentSystemAddress == null) {
            transactions.AddIncomplete(new MissionCompleted(),
                "Could not determine current location on Missions event.",
                entry
                );
            return;
        }

        foreach (Mission mission in missions.Active) {
            try {
                context.MissionAccepted(mission);
            } catch (Exception exception) {
                transactions.AddIncomplete(new MissionCompleted(),
                    exception.Message,
                    entry
                    );
            }
        }
    }
}

internal class MissionAcceptedParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        MissionAcceptedEntry? entry = e as MissionAcceptedEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (context.CurrentSystem == null || context.CurrentSystemAddress == null) {
            transactions.AddIncomplete(new MissionCompleted(),
                "Could not determine current location on mission acceptance.",
                e
                );
            return;
        }

        try {
            context.MissionAccepted(entry);
        } catch (Exception exception) {
            transactions.AddIncomplete(new MissionCompleted(),
                exception.Message,
                e
                );
        }
    }
}

internal class MissionCompletedParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        MissionCompletedEntry? entry = e as MissionCompletedEntry;
        if (entry == null || entry.Mission == null) {
            throw new NotImplementedException();
        }

        Mission? mission = null;
        Location? accepted_location = null;
        string? target_faction_name = entry.Mission.TargetFaction;
        string? source_faction_name = entry.Mission.Faction;

        // We did not find when the mission was accepted.
        if (!context.AcceptedMissions.TryGetValue(entry.Mission.MissionID, out mission)) {
            transactions.AddIncomplete(new MissionCompleted(),
                String.Format("Mission acceptance for mission id {0} was not found",
                entry.Mission.MissionID), e);
            return;
        }

        if (!context.AcceptedMissionLocation.TryGetValue(entry.Mission.MissionID, out accepted_location)) {
            transactions.AddIncomplete(new MissionCompleted(),
                String.Format("Location for acceptance for mission id {0} was not found",
                entry.Mission.MissionID), e);
            return;
        }

        // This block does some preliminary "repairs" on the influences block of a completed
        // mission. Sometimes these entries are broken, or are missing information for later
        // parsing.
        foreach (var other in entry.Mission.Influences) {
            string faction = other.Key;
            if (string.IsNullOrEmpty(faction)) {
                // Target faction might be empty string, in special cases. For example if you
                // scan a surface installation, and the target faction of the surface installation
                // gets negative REP, but the surface installation is not owned by anyone.
                continue;
            }

            // Fun ahead: sometimes the influence list is empty for a faction entry. Here
            // we try to repair it.
            if (other.Value.Count == 0) {
                if (string.Compare(target_faction_name, faction, true) == 0) {
                    if (context.CurrentSystemAddress == null) {
                        continue;
                    }
                    other.Value.Add(context.CurrentSystemAddress.Value, "");
                    // Mission gave no influence to the target faction, so we assume
                    // the target faction was in the same system.
                } else if (string.Compare(source_faction_name, faction, true) == 0) {
                    // This happens if the source faction is not getting any influence
                    // This could be if the source faction is in a conflict, and thus does
                    // not gain any influence at all.
                    other.Value.Add(accepted_location.SystemAddress, "");

                    // Just check if the target/source faction are the same, in which case
                    // we also have to make an additional entry
                    if (string.Compare(source_faction_name, target_faction_name, true) == 0 &&
                        context.CurrentSystemAddress != null) {
                        other.Value.Add(context.CurrentSystemAddress.Value, "");
                    }
                }
            }

            // Now actually parse completed mission
            foreach (var influences in other.Value) {
                ulong system_address = influences.Key;
                string? system;

                if (!context.SystemsByID.TryGetValue(system_address, out system)) {
                    transactions.AddIncomplete(new MissionCompleted(),
                        string.Format("Unknown system {0}, unable to assign that mission a target", system_address),
                        e
                        );
                    continue;
                }

                if (string.Compare(faction, source_faction_name, true) == 0 &&
                    system_address == accepted_location.SystemAddress) {
                    // Source and target faction are the same, and this is the block
                    // for the source system. So we make a full mission completed entry.
                    transactions.Add(new MissionCompleted() {
                        CompletedEntry = entry,
                        Mission = mission,
                        System = accepted_location.StarSystem,
                        Faction = source_faction_name,
                        SystemAddress = accepted_location.SystemAddress,
                        Station = accepted_location.Station,
                        IsLegacy = context.IsLegacy,
                    });
                } else if (string.Compare(faction, source_faction_name, true) != 0 ||
                    (string.Compare(faction, source_faction_name) == 0 && 
                     system_address != accepted_location.SystemAddress)) {
                    // Source or target faction are different, and/or the system
                    // differs. Sometimes missions go to different systems but to
                    // the same faction.
                    transactions.Add(new InfluenceSupport() {
                        Mission = mission,
                        Faction = faction,
                        Influence = influences.Value,
                        System = system,
                        SystemAddress = system_address,
                        RelevantMission = entry,
                        IsLegacy = context.IsLegacy,
                    });
                }
            }
        }
    }
}

internal class MissionFailedParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        Mission? mission = null;
        Location? accepted_location = null;
        string? accepted_system = null;

        MissionFailedEntry? entry = e as MissionFailedEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (entry.Mission == null) {
            throw new InvalidJournalEntryException("No mission specified in mission failure");
        }

        if (!context.AcceptedMissions.TryGetValue(entry.Mission.MissionID, out mission)) {
            transactions.AddIncomplete(new MissionFailed(),
                "Mission acceptance was not found", e
                );
            return;
        }

        if (!context.AcceptedMissionLocation.TryGetValue(mission.MissionID, out accepted_location)) {
            transactions.AddIncomplete(new MissionFailed(),
                "Unable to figure out where failed mission was accepted", e
                );
            return;
        }

        if (!context.SystemsByID.TryGetValue(accepted_location.SystemAddress, out accepted_system)) {
            transactions.AddIncomplete(new MissionFailed(),
                "Unable to figure out in which system failed mission was accepted", e
                );
            return;
        }

        if (string.IsNullOrEmpty(mission.Faction)) {
            transactions.AddIncomplete(new MissionFailed(),
                "Could not determine for what faction you failed a mission. This happens if the " +
                "mission acceptance is not within the given time frame.",
                entry
                );
        }

        transactions.Add(new MissionFailed(entry) {
            Faction = mission?.Faction,
            Mission = mission,
            Station = accepted_location.Station,
            System = accepted_location.StarSystem,
            SystemAddress = accepted_location.SystemAddress,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class SellExplorationDataParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        SellExplorationDataEntry? entry = e as SellExplorationDataEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        transactions.Add(new Cartographics(entry) {
            System = context.CurrentSystem,
            Station = context.CurrentStation,
            Faction = context.ControllingFaction,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class SellOrganicDataParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        SellOrganicDataEntry? entry = e as SellOrganicDataEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        transactions.Add(new OrganicData(entry) {
            System = context.CurrentSystem,
            Station = context.CurrentStation,
            Faction = context.ControllingFaction,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class MultiSellExplorationDataParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        MultiSellExplorationDataEntry? entry = e as MultiSellExplorationDataEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        transactions.Add(new Cartographics(entry) {
            System = context.CurrentSystem,
            Station = context.CurrentStation,
            Faction = context.ControllingFaction,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class RedeemVoucherParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        RedeemVoucherEntry? entry = e as RedeemVoucherEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (context.CurrentSystem == null) {
            transactions.AddIncomplete(new Vouchers(),
                "Could not find out where the vouchers were redeemed", e
                );
            return;
        }

        List<Faction>? current_factions = context.GetFactions(context.CurrentSystem);
        if (current_factions == null) {
            transactions.AddIncomplete(new Vouchers(),
                "Current system factions are unknown, so vouchers were ineffective", e);
        }

        foreach (string faction in entry.Factions) {
            bool relevantBond = false;
            string relevantFaction = faction;

            if (string.Compare(faction, Factions.PilotsFederationVouchers) == 0) {
                // Target faction is pilots' federation, so we assume thargoid bonks
                // Also assign this combat bond to the Pilots Federation
                relevantFaction = Factions.PilotsFederation;
                relevantBond = true;
            }

            if (current_factions != null && !relevantBond) {
                // If we have local factions, and it ain't thargoid bonds see if the bonds were
                // useful in the current system
                if (current_factions.Find(x => string.Compare(x.Name, faction, true) == 0) != null) {
                    relevantBond = true;
                } else {
                    transactions.AddIncomplete(new Vouchers(),
                        string.Format("Vouchers for \"{0}\" had no effect in \"{1}\" since said " +
                        "faction is not present there", faction, context.CurrentSystem), e
                    );
                }
            }

            if (relevantBond) {
                transactions.Add(new Vouchers(entry) {
                    System = context.CurrentSystem,
                    Station = context.CurrentStation,
                    Faction = relevantFaction,
                    ControllingFaction = context.ControllingFaction,
                    IsLegacy = context.IsLegacy,
                });
            }
        }
    }
}

internal class SellMicroResourcesParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        SellMicroResourcesEntry? entry = e as SellMicroResourcesEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        transactions.Add(new SellMicroResources(entry) {
            System = context.CurrentSystem,
            Station = context.CurrentStation,
            Faction = context.ControllingFaction,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class SearchAndRescueParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        SearchAndRescueEntry? entry = e as SearchAndRescueEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        transactions.Add(new SearchAndRescue(entry) {
            Faction = context.ControllingFaction,
            Station = context.CurrentStation,
            System = context.CurrentSystem,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class MarketBuyParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        MarketBuyEntry? entry = e as MarketBuyEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        context.BoughtCargo(entry.Type, entry.BuyPrice);

        transactions.Add(new BuyCargo(entry) {
            Faction = context.ControllingFaction,
            Station = context.CurrentStation,
            System = context.CurrentSystem,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class MarketSellParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        long profit = 0;

        MarketSellEntry? entry = e as MarketSellEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (entry.Type == null) {
            throw new InvalidJournalEntryException("market sell contains no cargo type");
        }

        if (context.BuyCost.ContainsKey(entry.Type)) {
            long avg = context.BuyCost[entry.Type];
            profit = (long)entry.TotalSale - (avg * entry.Count);
        }

        transactions.Add(new SellCargo(entry) {
            Faction = context.ControllingFaction,
            Station = context.CurrentStation,
            System = context.CurrentSystem,
            Profit = profit,
            IsLegacy = context.IsLegacy,
        });
    }
}

internal class FactionKillBondParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        FactionKillBondEntry? entry = e as FactionKillBondEntry;
        if (entry == null) {
            throw new NotImplementedException();
        }

        if (Factions.IsThargoid(entry.VictimFaction)) {
            // Thargoid bonk
            transactions.Add(new ThargoidKill(entry) {
                System = context.CurrentSystem,
                Faction = Factions.PilotsFederation,
                Station = context.CurrentStation,
                IsLegacy = context.IsLegacy,
            });

            ThargoidVessel vessel = Thargoid.GetVesselByPayout(entry.Reward);
            if (vessel != ThargoidVessel.Unknown) {
                if (vessel == ThargoidVessel.Scout) {
                    ++context.ThargoidScoutKills;
                } else {
                    ++context.ThargoidInterceptorKills;
                }
            }

            // We are done
            return;
        }

        context.RecordCombatBond(entry);
    }
}

internal class EmbarkDisembarkParser : TransactionParserPart {
    public void Parse(Entry e, TransactionParserContext context, TransactionList transactions) {
        if (e.Is(Events.Embark)) {
            context.IsOnFoot = false;
        } else if (e.Is(Events.Disembark)) {
            context.IsOnFoot = true;
        }
    }
}

internal class SupercruiseEntryParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        // After a super cruise entry we are no longer on foot.
        context.IsOnFoot = false;
        context.DiscernCombatZone(transactions, entry);
        context.ResetCombatZone();
    }
}

internal class ShutdownParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        context.DiscernCombatZone(transactions, entry);
        context.ResetCombatZone();
    }
}

internal class CapShipBondParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        if (entry.GetType() != typeof(CapShipBondEntry)) {
            return;
        }

        context.HaveSeenCapShip = true;
    }
}

internal class FileHeaderParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        FileHeaderEntry? fileheader = entry as FileHeaderEntry;

        if (fileheader == null) {
            return;
        }

        context.IsLegacy = fileheader.IsLegacy;
    }
}

internal class ReceiveTextParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        ReceiveTextEntry? receivetext = entry as ReceiveTextEntry;

        if (receivetext == null) {
            return;
        }

        if (string.Compare(receivetext.Channel, Channels.NPC) != 0) {
            return;
        }

        if (string.Compare(receivetext.NPCCategory, NPCs.AXMilitary) == 0) {
            context.HaveSeenAXWarzoneNPC = true;
        }
    }
}

internal class DiedParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        // You can't complete a combat zone if you die in it. Others might keep it open
        // for you, but still you will not have completed it unless you jump back in.
        context.ResetCombatZone();
    }
}

internal class DropshipDeployParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        // On drop ship deploy we are now on foot
        context.IsOnFoot = true;
    }
}

internal class CommanderParser : TransactionParserPart {
    public void Parse(Entry entry, TransactionParserContext context, TransactionList transactions) {
        // A commander entry happens when you log out, and log back in again
        // for example when switching from Open, to Solo or PG.
        context.DiscernCombatZone(transactions, entry);
        context.ResetCombatZone();
    }
}

public class TransactionParser {
    private static Dictionary<string, TransactionParserPart> ParserParts { get; } = new()
    {
        { Events.CapShipBond, new CapShipBondParser() },
        { Events.Commander, new CommanderParser() },
        { Events.CommitCrime, new CommitCrimeParser() },
        { Events.Died, new DiedParser() },
        { Events.Disembark, new EmbarkDisembarkParser() },
        { Events.Docked, new DockedParser() },
        { Events.DropshipDeploy, new DropshipDeployParser() },
        { Events.Embark, new EmbarkDisembarkParser() },
        { Events.FactionKillBond, new FactionKillBondParser() },
        { Events.FileHeader, new FileHeaderParser() },
        { Events.FSDJump, new FSDJumpParser() },
        { Events.Location, new LocationParser() },
        { Events.MarketBuy, new MarketBuyParser() },
        { Events.MarketSell, new MarketSellParser() },
        { Events.MissionAccepted, new MissionAcceptedParser() },
        { Events.MissionCompleted, new MissionCompletedParser() },
        { Events.MissionFailed, new MissionFailedParser() },
        { Events.Missions, new MissionsParser() },
        { Events.MultiSellExplorationData, new MultiSellExplorationDataParser() },
        { Events.ReceiveText, new ReceiveTextParser() },
        { Events.RedeemVoucher, new RedeemVoucherParser() },
        { Events.SearchAndRescue, new SearchAndRescueParser() },
        { Events.SellExplorationData, new SellExplorationDataParser() },
        { Events.SellMicroResources, new SellMicroResourcesParser() },
        { Events.SellOrganicData, new SellOrganicDataParser() },
        { Events.ShipTargeted, new ShipTargetedParser() },
        { Events.Shutdown, new ShutdownParser() },
        { Events.SupercruiseEntry, new SupercruiseEntryParser() },
    };

    public bool IsRelevant(string entry) {
        return ParserParts.ContainsKey(entry);
    }

    public bool IsRelevant(Entry e) {
        if (e.Event == null) {
            return false;
        }
        return IsRelevant(e.Event);
    }

    public List<Transaction>? Parse(IEnumerable<Entry> entries) {
        TransactionList transactions = new();
        TransactionParserContext context = new();

        foreach (Entry entry in entries) {
            if (entry.Event == null) {
                throw new InvalidJournalEntryException();
            }

            if (!ParserParts.ContainsKey(entry.Event)) {
                continue;
            }

            TransactionParserPart transactionParserPart = ParserParts[entry.Event];
            transactionParserPart.Parse(entry, context, transactions);
        }

        return transactions.ToList();
    }
}