package opendota;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

class ParsedData {
    public Integer version = 22;
    public Long match_id = 0L;
    public List<Teamfight> teamfights = new ArrayList<>();
    public List<Pause> pauses = new ArrayList<>();
    public List<Entry> objectives = new ArrayList<>();
    public List<Entry> chat = new ArrayList<>();
    public List<Integer> radiant_gold_adv = new ArrayList<>();
    public List<Integer> radiant_xp_adv = new ArrayList<>();
    public Map<String, Integer> cosmetics = new HashMap<>();
    public List<Object> draft_timings = new ArrayList<>();
    public List<PlayerData> players = new ArrayList<>();

    public ParsedData() {
        for (int i = 0; i < 10; i++) {
            this.players.add(new PlayerData());
        }
    }
}

class PlayerData {
    public Integer player_slot = 0;
    public Integer obs_placed = 0;
    public Integer sen_placed = 0;
    public Integer creeps_stacked = 0;
    public Integer camps_stacked = 0;
    public Integer rune_pickups = 0;
    public Integer firstblood_claimed = 0;
    public Float teamfight_participation = 0.0f;
    public Integer towers_killed = 0;
    public Integer roshans_killed = 0;
    public Integer observers_placed = 0;
    public Float stuns = 0.0f;
    public Map<String, Integer> performance_others;
    public Entry max_hero_hit = new Entry();
    public List<Integer> times = new ArrayList<>();
    public List<Integer> gold_t = new ArrayList<>();
    public List<Integer> lh_t = new ArrayList<>();
    public List<Integer> dn_t = new ArrayList<>();
    public List<Integer> xp_t = new ArrayList<>();
    public List<Entry> obs_log = new ArrayList<>();
    public List<Entry> sen_log = new ArrayList<>();
    public List<Entry> obs_left_log = new ArrayList<>();
    public List<Entry> sen_left_log = new ArrayList<>();
    public List<Map<String, Object>> purchase_log = new ArrayList<>();
    public List<Map<String, Object>> kills_log = new ArrayList<>();
    public List<Entry> buyback_log = new ArrayList<>();
    public List<Map<String, Object>> runes_log = new ArrayList<>();
    public List<Entry> connection_log = new ArrayList<>();
    public Map<String, HashMap<String, Integer>> lane_pos = new HashMap<>();
    public Map<String, HashMap<String, Integer>> obs = new HashMap<>();
    public Map<String, HashMap<String, Integer>> sen = new HashMap<>();
    public Map<String, Integer> actions = new HashMap<>();
    public Map<String, Integer> pings = new HashMap<>();
    public Map<String, Integer> purchase = new HashMap<>();
    public Map<String, Integer> gold_reasons = new HashMap<>();
    public Map<String, Integer> xp_reasons = new HashMap<>();
    public Map<String, Integer> killed = new HashMap<>();
    public Map<String, Integer> item_uses = new HashMap<>();
    public Map<String, Integer> ability_uses = new HashMap<>();
    public Map<String, HashMap<String, Integer>> ability_targets = new HashMap<>();
    public Map<String, HashMap<String, Integer>> damage_targets = new HashMap<>();
    public Map<String, Integer> hero_hits = new HashMap<>();
    public Map<String, Integer> damage = new HashMap<>();
    public Map<String, Integer> damage_taken = new HashMap<>();
    public Map<String, Integer> damage_inflictor = new HashMap<>();
    public Map<String, Integer> runes = new HashMap<>();
    public Map<String, Integer> killed_by = new HashMap<>();
    public Map<String, Integer> kill_streaks = new HashMap<>();
    public Map<String, Integer> multi_kills = new HashMap<>();
    public Map<String, Integer> life_state = new HashMap<>();
    public Map<String, Integer> healing = new HashMap<>();
    public Map<String, Integer> damage_inflictor_received = new HashMap<>();
    public Boolean randomed = false;
    public Boolean repicked = false;
    public Boolean pred_vict = false;
    public List<Map<String, Object>> neutral_tokens_log = new ArrayList<>();
    public List<Map<String, Object>> neutral_item_history = new ArrayList<>();
}

class Metadata {
    public HashMap<String, Integer> hero_to_slot = new HashMap<>();
    public HashMap<Integer, Integer> slot_to_player_slot = new HashMap<Integer, Integer>();
    public HashMap<Integer, Integer> hero_id_to_slot = new HashMap<Integer, Integer>();
    public HashMap<String, HashMap<String, Integer>> ability_levels = new HashMap<String, HashMap<String, Integer>>();
}

class Pause {
    public Integer time;
    public Integer duration;
}

class AllPlayersResult {
    public List<Integer> radiantGoldAdv;
    public List<Integer> radiantXpAdv;
}

class Teamfight {
    public Integer start;
    public Integer end;
    public Integer last_death;
    public Integer deaths;
    public List<TeamfightPlayer> players = new ArrayList<TeamfightPlayer>();

    public Teamfight() {
        for (int i = 0; i < 10; i++) {
            this.players.add(new TeamfightPlayer());
        }
    }
}

class TeamfightPlayer {
    public HashMap<String, HashMap<String, Integer>> deaths_pos = new HashMap<String, HashMap<String, Integer>>();
    public HashMap<String, Integer> ability_uses = new HashMap<>();
    public HashMap<String, Integer> ability_targets = new HashMap<>();
    public HashMap<String, Integer> item_uses = new HashMap<>();
    public HashMap<String, Integer> killed = new HashMap<>();
    public Integer deaths = 0;
    public Integer buybacks = 0;
    public Integer damage = 0;
    public Integer healing = 0;
    public Integer gold_delta = 0;
    public Integer xp_delta = 0;
    public Integer xp_start;
    public Integer xp_end;
}

public class CreateParsedDataBlob {

    private Gson g = new Gson();

    public ParsedData createParsedDataBlob(List<Entry> entries) {
        long tStart = System.currentTimeMillis();
        Metadata meta = processMetadata(entries);
        long tEnd = System.currentTimeMillis();
        System.err.format("metadata: %sms\n", tEnd - tStart);

        tStart = System.currentTimeMillis();
        List<Entry> expanded = processExpand(entries, meta);
        tEnd = System.currentTimeMillis();
        System.err.format("expand: %sms\n", tEnd - tStart);

        tStart = System.currentTimeMillis();
        ParsedData parsedData = processParsedData(expanded, new ParsedData(), meta);
        tEnd = System.currentTimeMillis();
        System.err.format("populate: %sms\n", tEnd - tStart);

        tStart = System.currentTimeMillis();
        parsedData.teamfights = processTeamfights(expanded, meta);
        tEnd = System.currentTimeMillis();
        System.err.format("teamfights: %sms\n", tEnd - tStart);

        tStart = System.currentTimeMillis();
        parsedData.pauses = processPauses(entries);
        tEnd = System.currentTimeMillis();
        System.err.format("pauses: %sms\n", tEnd - tStart);

        tStart = System.currentTimeMillis();
        AllPlayersResult ap = processAllPlayers(entries, meta);
        parsedData.radiant_gold_adv = ap.radiantGoldAdv;
        parsedData.radiant_xp_adv = ap.radiantXpAdv;
        tEnd = System.currentTimeMillis();
        System.err.format("processAllPlayers: %sms\n", tEnd - tStart);

        return parsedData;
    }

    private void greevilsGreed(Entry e, ParsedData container, Metadata meta) {
        if ("killed".equals(e.type) && e.greevils_greed_stack != null) {
            String alchName = "npc_dota_hero_alchemist";
            Integer alchSlot = meta.hero_to_slot.get(alchName);
            if (alchSlot != null) {
                PlayerData alchPlayer = container.players.get(alchSlot);
                int goldBase = 3;
                int goldStack = e.greevils_greed_stack * 3;
                goldStack = Math.min(goldStack, 18);

                if (alchPlayer.performance_others == null) {
                    alchPlayer.performance_others = new HashMap<>();
                }
                alchPlayer.performance_others.put("greevils_greed_gold",
                        alchPlayer.performance_others.getOrDefault("greevils_greed_gold", 0) + goldBase + goldStack);
            }
        }
    }

    private void performanceOthers(Entry e, ParsedData container, Metadata meta) {
        greevilsGreed(e, container, meta);
    }

    private void populate(Entry e, ParsedData container, Metadata meta) {
        switch (e.type) {
            case "interval":
                break;
            case "player_slot":
                container.players.get(Integer.valueOf(e.key)).player_slot = e.value;
                break;
            case "chat":
            case "chatwheel":
                container.chat.add(deepCopy(e, Entry.class));
                break;
            case "cosmetics":
                Type mapType = new TypeToken<Map<String, Integer>>() {
                }.getType();
                container.cosmetics = g.fromJson(e.key, mapType);
                break;
            case "CHAT_MESSAGE_FIRSTBLOOD":
            case "CHAT_MESSAGE_COURIER_LOST":
            case "CHAT_MESSAGE_AEGIS":
            case "CHAT_MESSAGE_AEGIS_STOLEN":
            case "CHAT_MESSAGE_DENIED_AEGIS":
            case "CHAT_MESSAGE_ROSHAN_KILL":
            case "CHAT_MESSAGE_MINIBOSS_KILL":
            case "building_kill":
                container.objectives.add(deepCopy(e, Entry.class));
                break;
            case "ability_levels":
                if (!meta.ability_levels.containsKey(e.unit)) {
                    meta.ability_levels.put(e.unit, new HashMap<>());
                }
                meta.ability_levels.get(e.unit).put(e.key, e.level);
                break;
            default:
                if (e.slot == null || e.slot < 0 || e.slot >= container.players.size()) {
                    return;
                }
                PlayerData player = container.players.get(e.slot);

                if (e.posData != null && e.posData) {
                    handlePosData(e, player);
                } else if (e.max != null && e.max) {
                    handleMaxValue(e, player);
                } else if ("ability_targets".equals(e.type)) {
                    handleAbilityTargets(e, player);
                } else if ("damage_targets".equals(e.type)) {
                    handleDamageTargets(e, player);
                } else if ("stuns".equals(e.type)) {
                    player.stuns = e.floatValue;
                } else if ("obs_placed".equals(e.type)) {
                    player.obs_placed = e.value;
                } else if ("sen_placed".equals(e.type)) {
                    player.sen_placed = e.value;
                } else if ("creeps_stacked".equals(e.type)) {
                    player.creeps_stacked = e.value;
                } else if ("camps_stacked".equals(e.type)) {
                    player.camps_stacked = e.value;
                } else if ("rune_pickups".equals(e.type)) {
                    player.rune_pickups = e.value;
                } else if ("randomed".equals(e.type)) {
                    player.randomed = e.booleanValue;
                } else if ("repicked".equals(e.type)) {
                    player.repicked = e.booleanValue;
                } else if ("pred_vict".equals(e.type)) {
                    player.pred_vict = e.booleanValue;
                } else if ("firstblood_claimed".equals(e.type)) {
                    player.firstblood_claimed = e.value;
                } else if ("teamfight_participation".equals(e.type)) {
                    player.teamfight_participation = e.floatValue;
                } else if ("towers_killed".equals(e.type)) {
                    player.towers_killed = e.value;
                } else if ("roshans_killed".equals(e.type)) {
                    player.roshans_killed = e.value;
                } else if ("observers_placed".equals(e.type)) {
                    player.observers_placed = e.value;
                } else if (isArrayField(e.type)) {
                    handleArrayField(e, player);
                } else if (isMapField(e.type)) {
                    handleMapField(e, player, container, meta);
                } else {
                    throw new RuntimeException("populate: unhandled entry type " + e.type);
                }
                break;
        }
    }

    private void handlePosData(Entry e, PlayerData player) {
        Map<String, HashMap<String, Integer>> targetMap = getPlayerPosData(player, e.type);
        int[] coords = g.fromJson(e.key, int[].class);
        String x = String.valueOf(coords[0]);
        String y = String.valueOf(coords[1]);
        HashMap<String, Integer> targetX = targetMap.get(x);
        if (targetX == null) {
            targetMap.put(x, new HashMap<>());
        }
        targetMap.get(x).put(y, targetMap.get(x).getOrDefault(y, 0) + 1);
    }

    private void handleMaxValue(Entry e, PlayerData player) {
        if (player.max_hero_hit.value == null || e.value > player.max_hero_hit.value) {
            player.max_hero_hit = e;
        }
    }

    private void handleArrayField(Entry e, PlayerData player) {
        if ("neutral_item_history".equals(e.type)) {
            handleNeutralItemHistory(e, player.neutral_item_history);
        } else if (e.interval != null && e.interval) {
            List<Integer> targetList = getPlayerIntegerList(player, e.type);
            targetList.add(e.value);
        } else if (Arrays.asList("purchase_log", "kills_log", "runes_log", "neutral_tokens_log").contains(e.type)) {
            Map<String, Object> obj = new HashMap<>();
            obj.put("time", e.time);
            obj.put("key", e.key);

            if ("purchase_log".equals(e.type)) {
                int maxCharges = "tango".equals(e.key) ? 3 : 1;
                if (e.charges != null && e.charges > maxCharges) {
                    obj.put("charges", e.charges);
                }
            }

            if ("kills_log".equals(e.type) && e.tracked_death != null) {
                obj.put("tracked_death", e.tracked_death);
                obj.put("tracked_sourcename", e.tracked_sourcename);
            }

            if ("purchase_log".equals(e.type)) {
                player.purchase_log.add(obj);
            } else if ("kills_log".equals(e.type)) {
                player.kills_log.add(obj);
            } else if ("runes_log".equals(e.type)) {
                player.runes_log.add(obj);
            } else if ("neutral_tokens_log".equals(e.type)) {
                player.neutral_tokens_log.add(obj);
            }
        } else {
            List<Entry> targetList = getPlayerEntryList(player, e.type);
            targetList.add(shallowCopy(e));
        }
    }

    private void handleNeutralItemHistory(Entry e, List<Map<String, Object>> targetList) {
        String itemName = e.key.replaceAll("([A-Z])", "_$1").toLowerCase().replaceAll("__", "_");
        if (itemName.startsWith("_")) {
            itemName = itemName.substring(1);
        }
        if ("enhancement_timelss".equals(itemName)) {
            itemName = "enhancement_timeless";
        }

        Map<String, Object> existing = null;
        for (Map<String, Object> obj : targetList) {
            if (e.time.equals(obj.get("time"))) {
                existing = obj;
                break;
            }
        }

        if (existing == null) {
            existing = new HashMap<>();
            existing.put("time", e.time);
            targetList.add(existing);
        }

        if (e.isNeutralActiveDrop != null && e.isNeutralActiveDrop) {
            existing.put("item_neutral", itemName);
        }
        if (e.isNeutralPassiveDrop != null && e.isNeutralPassiveDrop) {
            existing.put("item_neutral_enhancement", itemName);
        }
    }

    private void handleAbilityTargets(Entry e, PlayerData player) {
        String[] keyParts = g.fromJson(e.key, String[].class);
        String ability = keyParts[0];
        String target = keyParts[1];

        if (!player.ability_targets.containsKey(ability)) {
            player.ability_targets.put(ability, new HashMap<>());
        }
        Map<String, Integer> targets = player.ability_targets.get(ability);
        targets.put(target, targets.getOrDefault(target, 0) + 1);
    }

    private void handleDamageTargets(Entry e, PlayerData player) {
        String[] keyParts = g.fromJson(e.key, String[].class);
        String ability = keyParts[0];
        String target = keyParts[1];

        if (!player.damage_targets.containsKey(ability)) {
            player.damage_targets.put(ability, new HashMap<>());
        }
        Map<String, Integer> targets = player.damage_targets.get(ability);
        targets.put(target, targets.getOrDefault(target, 0) + e.value);
    }

    private void handleMapField(Entry e, PlayerData player, ParsedData container, Metadata meta) {
        Map<String, Integer> targetMap = getPlayerMap(player, e.type);
        int value = e.value != null ? e.value : 1;
        targetMap.put(e.key, targetMap.getOrDefault(e.key, 0) + value);
        performanceOthers(e, container, meta);
    }

    private AllPlayersResult processAllPlayers(List<Entry> entries, Metadata meta) {
        Map<Integer, Integer> goldAdvTime = new HashMap<>();
        Map<Integer, Integer> xpAdvTime = new HashMap<>();

        for (Entry e : entries) {
            if (e.time >= 0 && e.time % 60 == 0 && "interval".equals(e.type)) {
                Integer playerSlot = meta.slot_to_player_slot.get(e.slot);
                if (playerSlot != null) {
                    int g = isRadiant(playerSlot) ? e.gold : -e.gold;
                    int x = isRadiant(playerSlot) ? e.xp : -e.xp;

                    goldAdvTime.put(e.time, goldAdvTime.getOrDefault(e.time, 0) + g);
                    xpAdvTime.put(e.time, xpAdvTime.getOrDefault(e.time, 0) + x);
                }
            }
        }

        List<Integer> radiantGoldAdv = new ArrayList<>();
        List<Integer> radiantXpAdv = new ArrayList<>();

        List<Integer> sortedKeys = new ArrayList<>(goldAdvTime.keySet());
        Collections.sort(sortedKeys);

        for (Integer key : sortedKeys) {
            radiantGoldAdv.add(goldAdvTime.get(key));
            radiantXpAdv.add(xpAdvTime.get(key));
        }

        AllPlayersResult result = new AllPlayersResult();
        result.radiantGoldAdv = radiantGoldAdv;
        result.radiantXpAdv = radiantXpAdv;

        return result;
    }

    private boolean isRadiant(int playerSlot) {
        return playerSlot < 128;
    }

    private String translate(String input) {
        if ("dota_unknown".equals(input)) {
            return null;
        }
        if (input != null && input.startsWith("item_")) {
            return input.substring(5);
        }
        return input;
    }

    private String computeIllusionString(String input, Boolean isIllusion) {
        return (isIllusion != null && isIllusion ? "illusion_" : "") + input;
    }

    private List<Entry> processExpand(List<Entry> entries, Metadata meta) {
        List<Entry> output = new ArrayList<>();
        Integer aegisHolder = null;
        Integer aegisDeathTime = null;

        for (Entry e : entries) {
            String type = e.type;

            switch (type) {
                case "DOTA_COMBATLOG_DAMAGE":
                    handleDamageCombat(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_HEAL":
                    handleHeal(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_MODIFIER_ADD":
                    if ("modifier_aegis_regen".equals(e.inflictor)) {
                        aegisHolder = null;
                    }
                    break;
                case "DOTA_COMBATLOG_DEATH":
                    Integer[] result = handleDeathCombat(e, output, meta, aegisHolder, aegisDeathTime);
                    aegisHolder = result[0];
                    aegisDeathTime = result[1];
                    break;
                case "DOTA_COMBATLOG_ABILITY":
                    handleAbility(e, output, meta);
                    break;
                case "DOTA_ABILITY_LEVEL":
                    handleAbilityLevel(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_ITEM":
                    handleItemUse(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_GOLD":
                    handleGold(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_XP":
                    handleXp(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_PURCHASE":
                    handlePurchase(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_BUYBACK":
                    handleBuyback(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_MULTIKILL":
                    handleMultikill(e, output, meta);
                    break;
                case "DOTA_COMBATLOG_KILLSTREAK":
                    handleKillstreak(e, output, meta);
                    break;
                case "pings":
                    handlePings(e, output, meta);
                    break;
                case "actions":
                    handleActions(e, output, meta);
                    break;
                case "CHAT_MESSAGE_RUNE_PICKUP":
                    handleRunePickup(e, output, meta);
                    break;
                case "CHAT_MESSAGE_RECONNECT":
                    handleReconnect(e, output, meta);
                    break;
                case "CHAT_MESSAGE_DISCONNECT_WAIT_FOR_RECONNECT":
                    handleDisconnect(e, output, meta);
                    break;
                case "CHAT_MESSAGE_FIRSTBLOOD":
                    handleFirstblood(e, output, meta);
                    break;
                case "CHAT_MESSAGE_AEGIS":
                    aegisHolder = e.player1;
                    handleAegis(e, output, meta);
                    break;
                case "CHAT_MESSAGE_AEGIS_STOLEN":
                    aegisHolder = e.player1;
                    handleAegisStolen(e, output, meta);
                    break;
                case "CHAT_MESSAGE_DENIED_AEGIS":
                    handleAegisDenied(e, output, meta);
                    break;
                case "CHAT_MESSAGE_ROSHAN_KILL":
                    handleRoshanKill(e, output, meta);
                    break;
                case "CHAT_MESSAGE_MINIBOSS_KILL":
                    handleMinibossKill(e, output, meta);
                    break;
                case "CHAT_MESSAGE_COURIER_LOST":
                    handleCourierLost(e, output, meta);
                    break;
                case "chat":
                    expand(e, output, meta);
                    break;
                case "chatwheel":
                    expand(e, output, meta);
                    break;
                case "interval":
                    handleInterval(e, output, meta);
                    break;
                case "obs":
                    handleObs(e, output, meta);
                    break;
                case "sen":
                    handleSen(e, output, meta);
                    break;
                case "obs_left":
                    handleObsLeft(e, output, meta);
                    break;
                case "sen_left":
                    handleSenLeft(e, output, meta);
                    break;
                case "epilogue":
                case "player_slot":
                case "cosmetics":
                    expand(e, output, meta);
                    break;
                case "neutral_token":
                    handleNeutralToken(e, output, meta);
                    break;
                case "neutral_item_history":
                    expand(e, output, meta);
                    break;
                default:
                    // System.err.println("expand: (WARNING) new entry type " + e.type);
                    break;
            }
        }

        return output;
    }

    private void handleDamageCombat(Entry e, List<Entry> output, Metadata meta) {
        String unit = e.sourcename;
        String key = computeIllusionString(e.targetname, e.targetillusion);
        String inflictor = translate(e.inflictor);

        Entry damage = shallowCopy(e);
        damage.time = e.time;
        damage.value = e.value;
        damage.unit = unit;
        damage.key = key;
        damage.type = "damage";
        expand(damage, output, meta);

        if (e.targethero != null && e.targethero &&
                (e.targetillusion == null || !e.targetillusion)) {

            Entry damageTaken = new Entry();
            damageTaken.time = e.time;
            damageTaken.value = e.value;
            damageTaken.unit = key;
            damageTaken.key = unit;
            damageTaken.type = "damage_taken";
            expand(damageTaken, output, meta);

            Entry damageTargets = new Entry();
            damageTargets.value = e.value;
            damageTargets.unit = unit;
            damageTargets.key = "[\"" + inflictor + "\",\"" + translate(e.targetname) + "\"]";
            damageTargets.type = "damage_targets";
            expand(damageTargets, output, meta);

            Entry heroHits = new Entry();
            heroHits.time = e.time;
            heroHits.value = 1;
            heroHits.unit = unit;
            heroHits.key = inflictor;
            heroHits.type = "hero_hits";
            expand(heroHits, output, meta);

            if (!key.equals(unit)) {
                Entry damageInflictor = new Entry();
                damageInflictor.time = e.time;
                damageInflictor.value = e.value;
                damageInflictor.unit = unit;
                damageInflictor.key = inflictor;
                damageInflictor.type = "damage_inflictor";
                expand(damageInflictor, output, meta);

                Entry maxHeroHit = new Entry();
                maxHeroHit.type = "max_hero_hit";
                maxHeroHit.time = e.time;
                maxHeroHit.max = true;
                maxHeroHit.inflictor = inflictor;
                maxHeroHit.unit = unit;
                maxHeroHit.key = key;
                maxHeroHit.value = e.value;
                expand(maxHeroHit, output, meta);

                if (e.sourcename != null && e.sourcename.contains("npc_dota_hero_")) {
                    Entry damageInflictorReceived = new Entry();
                    damageInflictorReceived.time = e.time;
                    damageInflictorReceived.value = e.value;
                    damageInflictorReceived.type = "damage_inflictor_received";
                    damageInflictorReceived.unit = key;
                    damageInflictorReceived.key = inflictor;
                    expand(damageInflictorReceived, output, meta);
                }
            }
        }
    }

    private void handleHeal(Entry e, List<Entry> output, Metadata meta) {
        Entry heal = shallowCopy(e);
        heal.time = e.time;
        heal.value = e.value;
        heal.unit = e.sourcename;
        heal.key = computeIllusionString(e.targetname, e.targetillusion);
        heal.type = "healing";
        expand(heal, output, meta);
    }

    private Integer[] handleDeathCombat(Entry e, List<Entry> output, Metadata meta,
            Integer aegisHolder, Integer aegisDeathTime) {
        String unit = e.sourcename;
        String key = computeIllusionString(e.targetname, e.targetillusion);

        if (e.targetname != null && (e.targetname.contains("_tower") ||
                e.targetname.contains("_rax_") || e.targetname.contains("_healers") ||
                e.targetname.contains("_fort"))) {
            Entry buildingKill = new Entry();
            buildingKill.time = e.time;
            buildingKill.type = "building_kill";
            buildingKill.unit = unit;
            buildingKill.key = key;
            expand(buildingKill, output, meta);
        }

        Integer slotForKey = meta.hero_to_slot.get(key);
        if (slotForKey != null && slotForKey.equals(aegisHolder)) {
            if (aegisDeathTime == null) {
                aegisDeathTime = e.time;
                return new Integer[] { aegisHolder, aegisDeathTime };
            }
            if (!aegisDeathTime.equals(e.time)) {
                aegisDeathTime = null;
                aegisHolder = null;
            } else {
                return new Integer[] { aegisHolder, aegisDeathTime };
            }
        }

        if (e.attackername != null && e.attackername.equals(key)) {
            return new Integer[] { aegisHolder, aegisDeathTime };
        }

        if (e.targethero != null && e.targethero &&
                (e.targetillusion == null || !e.targetillusion)) {

            Entry killLog = new Entry();
            killLog.time = e.time;
            killLog.unit = unit;
            killLog.key = key;
            killLog.tracked_death = e.tracked_death;
            killLog.tracked_sourcename = e.tracked_sourcename;
            killLog.type = "kills_log";
            expand(killLog, output, meta);

            Entry killedBy = new Entry();
            killedBy.time = e.time;
            killedBy.unit = key;
            killedBy.key = unit;
            killedBy.type = "killed_by";
            expand(killedBy, output, meta);
        }

        Entry killed = shallowCopy(e);
        killed.time = e.time;
        killed.unit = unit;
        killed.key = key;
        killed.type = "killed";
        killed.value = 1;
        killed.tracked_death = e.tracked_death;
        killed.tracked_sourcename = e.tracked_sourcename;
        killed.greevils_greed_stack = e.greevils_greed_stack;
        expand(killed, output, meta);

        return new Integer[] { aegisHolder, aegisDeathTime };
    }

    private void handleAbility(Entry e, List<Entry> output, Metadata meta) {
        Entry abilityUse = new Entry();
        abilityUse.time = e.time;
        abilityUse.unit = e.attackername;
        abilityUse.key = translate(e.inflictor);
        abilityUse.type = "ability_uses";
        expand(abilityUse, output, meta);

        if (e.targethero != null && e.targethero &&
                (e.targetillusion == null || !e.targetillusion)) {
            Entry abilityTarget = new Entry();
            abilityTarget.time = e.time;
            abilityTarget.unit = e.attackername;
            abilityTarget.key = "[\"" + translate(e.inflictor) + "\",\"" + translate(e.targetname) + "\"]";
            abilityTarget.type = "ability_targets";
            expand(abilityTarget, output, meta);
        }
    }

    private void handleAbilityLevel(Entry e, List<Entry> output, Metadata meta) {
        Entry abilityLevel = new Entry();
        abilityLevel.time = e.time;
        abilityLevel.unit = e.targetname;
        abilityLevel.level = e.abilitylevel;
        abilityLevel.key = translate(e.valuename);
        abilityLevel.type = "ability_levels";
        expand(abilityLevel, output, meta);
    }

    private void handleItemUse(Entry e, List<Entry> output, Metadata meta) {
        Entry itemUse = new Entry();
        itemUse.time = e.time;
        itemUse.unit = e.attackername;
        itemUse.key = translate(e.inflictor);
        itemUse.type = "item_uses";
        expand(itemUse, output, meta);
    }

    private void handleGold(Entry e, List<Entry> output, Metadata meta) {
        Entry goldReason = new Entry();
        goldReason.time = e.time;
        goldReason.value = e.value;
        goldReason.unit = e.targetname;
        goldReason.key = String.valueOf(e.gold_reason);
        goldReason.type = "gold_reasons";
        expand(goldReason, output, meta);
    }

    private void handleXp(Entry e, List<Entry> output, Metadata meta) {
        Entry xpReason = new Entry();
        xpReason.time = e.time;
        xpReason.value = e.value;
        xpReason.unit = e.targetname;
        xpReason.key = String.valueOf(e.xp_reason);
        xpReason.type = "xp_reasons";
        expand(xpReason, output, meta);
    }

    private void handlePurchase(Entry e, List<Entry> output, Metadata meta) {
        String unit = e.targetname;
        String key = translate(e.valuename);

        Entry purchase = new Entry();
        purchase.time = e.time;
        purchase.value = 1;
        purchase.unit = unit;
        purchase.key = key;
        purchase.charges = e.charges;
        purchase.type = "purchase";
        expand(purchase, output, meta);

        if (key != null && !key.startsWith("recipe_")) {
