import { Attacked, Crit, Damage, Defense } from "../components/combat";
import { Effect, EffectTemplate } from "../components/effect";
import { getWorldRandom, Random } from "../components/random";
import { Health } from "../components/stat";
import { System, World } from "../core/world";

let hitEffectCounter = 0;

export interface HitEvent {
    attackerId: string;
    sourceId: string | null;
    amount: number;
    damageType?: string;
}

export class CombatSystem extends System {
    override update(world: World) {
        let random: Random | undefined;

        for (const [target] of world.query(Attacked)) {
            const healths = target.getAll(Health).sort((a, b) => b.priority - a.priority);
            if (healths.length === 0) {
                console.warn(`[CombatSystem] Target ${target.id} has no Health component`);
                for (const attack of target.getAll(Attacked)) target.remove(attack);
                continue;
            }

            let damageSum = 0;
            const hitEvents: HitEvent[] = [];
            let lastHit: HitEvent | null = null;

            for (const attack of target.getAll(Attacked)) {
                const { attackerId, sourceId } = attack.state;
                target.remove(attack);

                const attacker = world.getEntity(attackerId);
                if (!attacker) {
                    console.warn(`[CombatSystem] Attacker ${attackerId} not found`);
                    continue;
                }

                const source = sourceId ? world.getEntity(sourceId) : attacker;
                if (!source) {
                    console.warn(`[CombatSystem] Source ${sourceId} not found`);
                    continue;
                }

                const damage = source.get(Damage);
                if (!damage) {
                    console.warn(`[CombatSystem] No Damage on source ${source.id}`);
                    continue;
                }

                const { damageType, minDamage = 0, variance = 0 } = damage.state;
                let damageAmount = damage.value;

                if (variance > 0) {
                    if (!random) {
                        random = getWorldRandom(world);
                    }
                    const variedDamage = random.use(r => r.nextInt(-variance, variance + 1));

                    if (variedDamage > 0) {
                        damageAmount += variedDamage;
                    }
                }

                const crit = source.get(Crit);

                if (crit) {
                    const { chance } = crit.state;
                    if (!random) {
                        random = getWorldRandom(world);
                    }
                    const roll = random.use(r => r.nextFloat());
                    if (roll < chance) {
                        damageAmount *= crit.value;
                    }
                }

                const typeDefense = target.get(Defense, (c) => c.state.damageType === damageType);
                if (typeDefense) {
                    damageAmount -= typeDefense.value;
                }
                const generalDefense = target.get(Defense, (c) => c.state.damageType == null);
                if (generalDefense) {
                    damageAmount -= generalDefense.value;
                }

                damageAmount = Math.max(0, minDamage, damageAmount);

                // Apply on-hit effects from source onto target
                for (const component of source.getAll(EffectTemplate)) {
                    const s = component.state;
                    target.add(
                        new Effect({
                            target: s.target,
                            targetKey: s.targetKey,
                            delta: s.delta,
                            targetField: s.targetField,
                            duration: s.duration ?? undefined,
                            permanent: s.permanent,
                            gradual: s.gradual,
                            condition: s.condition ?? undefined,
                            stacking: s.stacking,
                            tag: s.tag ?? undefined,
                        }),
                        `__hit_${source.id}_${component.key}_${hitEffectCounter++}`,
                    );
                }

                damageSum += damageAmount;
                lastHit = { attackerId, sourceId, amount: damageAmount, damageType };
                hitEvents.push(lastHit);
            }

            if (damageSum === 0) continue;

            let totalBefore = 0;
            for (const pool of healths) totalBefore += pool.value;

            for (const hit of hitEvents) {
                let remaining = hit.amount;

                if (hit.damageType) {
                    for (const pool of healths) {
                        if (remaining <= 0) break;
                        if (pool.value <= 0) continue;
                        if (!pool.damageTypes.includes(hit.damageType)) continue;
                        const take = Math.min(pool.value, remaining);
                        pool.update(-take);
                        remaining -= take;
                    }
                }

                if (remaining > 0) {
                    for (const pool of healths) {
                        if (remaining <= 0) break;
                        if (pool.value <= 0) continue;
                        if (pool.damageTypes.length > 0) continue;
                        const take = Math.min(pool.value, remaining);
                        pool.update(-take);
                        remaining -= take;
                    }
                }
            }

            for (const info of hitEvents) {
                target.emit('Combat.hit', info);
            }

            let totalAfter = 0;
            for (const pool of healths) totalAfter += pool.value;

            if (totalBefore > 0 && totalAfter <= 0 && lastHit) {
                target.emit('Combat.killed', lastHit);
            }
        }
    }
}
