import type { Class } from '@common/types';
import type { RPGActions, RPGVariables } from '../types';
import { ACTION_KEYS, getComponentName, STATE_KEYS, VARIABLE_KEYS } from '../utils/decorators';

interface EntityEvent<T = unknown> {
    target: Entity;
    data?: T;
}
interface WorldEvent<T = unknown> {
    target: World;
    data?: T;
}

type EntityEventHandler = <T>(event: EntityEvent<T>) => void;
type WorldEventHandler = <T>(event: WorldEvent<T>) => void;

export type EvalContext = Entity | World;

/** Symbol used by Serialization to access World's entity counter. */
export const WORLD_ENTITY_COUNTER = Symbol('rpg.world.entityCounter');
export const COMPONENT_KEY = Symbol('rpg.component.key');

export abstract class Component<TState = Record<string, unknown>> {
    entity!: Entity;

    private _state!: TState;
    private _key!: string | symbol;

    constructor(state: TState) {
        this._state = state;
    }

    get state(): TState { return this._state; }
    protected set state(state: TState) { this._state = state; }

    get key(): string {
        return typeof this._key === 'symbol'
            ? getComponentName(this.constructor) ?? this.constructor.name
            : this._key;
    }
    get [COMPONENT_KEY](): string | symbol { return this._key; }
    set key(key: string | symbol) { this._key = key; }

    get world(): World { return this.entity.world; }

    protected emit(event: string, data?: unknown): void {
        const componentKey = this.key;
        const componentName = getComponentName(this.constructor);
        const key = componentName && componentName !== componentKey
            ? `${componentName}(${componentKey})`
            : componentKey;

        this.entity.emit(`${key}.${event}`, data);
    }

    onAdd(): void { }
    onRemove(): void { }

    getVariables(): RPGVariables {
        const meta = (this.constructor as Function)[Symbol.metadata];
        const keys = meta?.[VARIABLE_KEYS] as Map<string | symbol, string> | undefined;
        const stateKeys = meta?.[STATE_KEYS] as Set<string> | undefined;
        const vars: RPGVariables = {};
        if (keys) {
            for (const [methodKey, exportName] of keys) {
                const v = (this as Record<string, unknown>)[String(methodKey)];
                if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') {
                    vars[exportName] = v;
                }
            }
        }
        if (stateKeys) {
            for (const key of stateKeys) {
                const v = (this._state as Record<string, unknown>)[String(key)];
                if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean') {
                    vars[key] = v;
                }
            }
        }
        return vars;
    }

    getActions(): RPGActions {
        const meta = (this.constructor as Function)[Symbol.metadata];
        const keys = meta?.[ACTION_KEYS] as Set<string | symbol> | undefined;
        if (!keys) return {};
        const actions: RPGActions = {};
        for (const key of keys) {
            const fn = (this as Record<string, unknown>)[String(key)];
            if (typeof fn === 'function') {
                actions[String(key)] = fn.bind(this);
            }
        }
        return actions;
    }
}

export abstract class System {
    onAdd(_world: World): void { }
    onRemove(_world: World): void { }
    update(_world: World, _dt: number): void { };
}

type ComponentFilter<T> = (component: T) => boolean;

export class Entity {
    readonly #components = new Map<string | symbol, Component>();

    constructor(
        readonly id: string,
        readonly world: World,
    ) { }

    add<T extends Component<any>>(component: T, k?: string): T {
        const key = k ?? Symbol();

        if (component == null) {
            throw new Error(`Component must be an instance of Component`);
        }
        const existing = this.#components.get(key);
        if (existing) existing.onRemove();
        component.entity = this;
        component.key = key;
        this.#components.set(key, component);
        component.onAdd();
        return component;
    }

    clone<T extends Component<any>>(component: T, key: string): T {
        const clone = Object.create(component.constructor.prototype) as T;
        (clone as unknown as { state: unknown }).state = structuredClone(component.state);
        return this.add(clone, key);
    }

    get<T extends Component<any>>(key: string): T | undefined;
    get<T extends Component<any>>(ctor: Class<T>, key?: string): T | undefined;
    get<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): T | undefined;
    get<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string | ComponentFilter<T>): T | undefined {
        if (typeof ctorOrKey === 'string') {
            return this.#components.get(ctorOrKey) as T | undefined;
        }
        if (typeof key === 'string') {
            const c = this.#components.get(key);
            return c instanceof ctorOrKey ? c as T : undefined;
        }
        if (!key) { 
            for (const [k, c] of this.#components) {
                // prefer registered without key
                if (typeof k !== 'symbol') continue;
                if (c instanceof ctorOrKey) {
                    return c as T;
                }
            }
        }
        for (const c of this.#components.values()) {
            if (!(c instanceof ctorOrKey)) continue;
            if (typeof key === 'function' && !key(c)) continue;

            return c as T;
        }
        return undefined;
    }

    getAll<T extends Component<any>>(ctor: Class<T>): T[] {
        const result: T[] = [];
        for (const c of this.#components.values()) {
            if (c instanceof ctor) result.push(c as T);
        }
        return result;
    }

    has(key: string): boolean;
    has<T extends Component<any>>(ctor: Class<T>): boolean;
    has<T extends Component<any>>(ctor: Class<T>, key: string): boolean;
    has<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): boolean;
    has<T extends Component<any>>(ctorOrKey: Class<T> | string, key?: string | ComponentFilter<T>): boolean {
        if (typeof ctorOrKey === 'string') {
            return this.#components.has(ctorOrKey);
        }
        if (typeof key === 'string') {
            return this.#components.get(key) instanceof ctorOrKey;
        }
        for (const c of this.#components.values()) {
            if (!(c instanceof ctorOrKey)) continue;
            if (typeof key === 'function' && !key(c)) continue;

            return true;
        }
        return false;
    }

    remove(key: string): void;
    remove<T extends Component<any>>(component: T): void;
    remove<T extends Component<any>>(ctor: Class<T>): void;
    remove<T extends Component<any>>(ctor: Class<T>, key: string): void;
    remove<T extends Component<any>>(ctor: Class<T>, filter: ComponentFilter<T>): void;
    remove<T extends Component<any>>(ctorOrKey: Class<T> | T | string, key?: string | ComponentFilter<T>): void {
        if (typeof ctorOrKey === 'string') {
            this.#removeByKey(ctorOrKey);
            return;
        }
        if (ctorOrKey instanceof Component) {
            this.#removeByKey(ctorOrKey[COMPONENT_KEY]);
            return;
        }
        if (typeof key === 'string') {
            if (this.#components.get(key) instanceof ctorOrKey) this.#removeByKey(key);
            return;
        }
        for (const [k, c] of this.#components) {
            if (!(c instanceof ctorOrKey)) continue;
            if (typeof key === 'function' && !key(c)) continue;

            this.#removeByKey(k); return;
        }
    }

    removeAll<T extends Component<any>>(ctor: Class<T>, filter?: ComponentFilter<T>): void {
        for (const [k, c] of this.#components) {
            if (!(c instanceof ctor)) continue;
            if (typeof filter === 'function' && !filter(c)) continue;

            this.#removeByKey(k); return;
        }
    }

    #removeByKey(key: string | symbol): void {
        const c = this.#components.get(key);
        if (c) { c.onRemove(); this.#components.delete(key); }
    }

    emit(event: string, data?: unknown): void {
        this.world.emit(this.id, event, data);
    }

    emitGlobal(event: string, data?: unknown): void {
        this.world.emitGlobal(event, data);
    }

    on(event: string, handler: EntityEventHandler): () => void {
        return this.world.on(this.id, event, handler);
    }

    off(event: string, handler: EntityEventHandler): void {
        this.world.off(this.id, event, handler);
    }

    once(event: string, handler: EntityEventHandler): () => void {
        return this.world.once(this.id, event, handler);
    }

    destroy(): void {
        this.world.destroyEntity(this);
    }

    /** @internal */
    [Symbol.iterator](): IterableIterator<[string, Component]> {
        return this.#components.values().map(c => [c.key, c]);
    }

    /** @internal */
    _destroy(): void {
        for (const c of this.#components.values()) c.onRemove();
        this.#components.clear();
    }
}

export class World {
    /** World-level variables, accessible in conditions via the $. prefix */
    readonly globals: RPGVariables = {};

    readonly #entities = new Map<string, Entity>();
    readonly #handlers = new Map<string, Set<EntityEventHandler>>();
    readonly #globalHandlers = new Map<string, Set<WorldEventHandler>>();
    readonly #onceWrappers = new Map<Function, Function>();
    readonly #systems: System[] = [];
    #entityCounter = 0;

    get [WORLD_ENTITY_COUNTER](): number { return this.#entityCounter; }
    set [WORLD_ENTITY_COUNTER](n: number) { this.#entityCounter = n; }

    get id() { return 'world'; }
    get world() { return this; }

    /**
     * Create a new entity and add it to the world.
     *
     * @param id - How the entity ID is determined:
     *   - Omitted → auto-generated: `entity_1`, `entity_2`, …
     *   - Plain string → used as-is: `createEntity('player')` → `'player'`
     *   - Template with `*` → `*` is replaced by the auto-incremented counter:
     *     `createEntity('enemy_*')` → `'enemy_1'`, `'enemy_2'`, …
     * @throws If an entity with the resolved ID already exists.
     */
    createEntity(id?: string): Entity {
        const entityId = id == null
            ? `entity_${++this.#entityCounter}`
            : id.includes('*') ? id.replace('*', String(++this.#entityCounter)) : id;
        if (this.#entities.has(entityId)) throw new Error(`Entity '${entityId}' already exists`);
        const entity = new Entity(entityId, this);
        this.#entities.set(entityId, entity);
        return entity;
    }

    getEntity(id: string): Entity | undefined {
        return this.#entities.get(id);
    }

    destroyEntity(entity: Entity): void {
        entity._destroy();
        this.#entities.delete(entity.id);
        for (const key of this.#handlers.keys()) {
            if (key.startsWith(`${entity.id}\0`)) this.#handlers.delete(key);
        }
    }

    /**
     * Create a new entity whose components are deep copies of `source`'s components.
     * The clone is immediately live in the world and `onAdd()` fires for each component.
     * @param source - Entity to clone.
     * @param newId - ID for the clone; follows the same rules as {@link createEntity}.
     */
    cloneEntity(source: Entity, newId?: string): Entity {
        const target = this.createEntity(newId);
        for (const [key, component] of source) {
            const clone = Object.create(component.constructor.prototype) as Component<any>;
            (clone as unknown as { state: unknown }).state = structuredClone(component.state);
            target.add(clone, key);
        }
        return target;
    }

    findComponent<T extends Component<any>>(ctor: Class<T>, filter?: ComponentFilter<T>): T | undefined {
        for (const [, , c] of this.query(ctor)) {
            if (!filter || filter(c)) return c;
        }
        return undefined;
    }

    query<T extends Component<any>>(ctor: Class<T>): Generator<[Entity, string, T]>;
    query<A extends Component<any>, B extends Component<any>>(ctorA: Class<A>, ctorB: Class<B>): Generator<[Entity, A, B]>;
    query<A extends Component<any>, B extends Component<any>, C extends Component<any>>(ctorA: Class<A>, ctorB: Class<B>, ctorC: Class<C>): Generator<[Entity, A, B, C]>;
    *query(...ctors: Class<Component<any>>[]): Generator<unknown[]> {
        const entities = this.#entities;
        if (ctors.length === 1) {
            const ctor = ctors[0];
            for (const entity of entities.values()) {
                for (const [key, component] of entity) {
                    if (component instanceof ctor) yield [entity, key, component];
                }
            }
        } else {
            for (const entity of entities.values()) {
                const components = ctors.map(ctor => entity.get(ctor));
                if (components.every(c => c !== undefined)) yield [entity, ...components];
            }
        }
    }

    addSystem<T extends System>(system: T): T {
        this.#systems.push(system);
        system.onAdd(this);
        return system;
    }

    removeSystem(system: System): void {
        const idx = this.#systems.indexOf(system);
        if (idx !== -1) {
            this.#systems.splice(idx, 1);
            system.onRemove(this);
        }
    }

    hasSystem(ctor: Class<System>): boolean {
        return this.#systems.some(system => system instanceof ctor);
    }

    update(dt: number) {
        for (const system of this.#systems) {
            system.update(this, dt);
        }
    }

    emit(entityId: string, event: string, data?: unknown): void {
        const entity = this.getEntity(entityId);
        if (!entity) return;
        this.#handlers.get(`${entityId}\0${event}`)?.forEach(h => h({ target: entity, data }));
    }

    emitGlobal(event: string, data?: unknown): void {
        this.#globalHandlers.get(event)?.forEach(h => h({ target: this, data }));
    }

    on(event: string, handler: WorldEventHandler): () => void;
    on(entityId: string, event: string, handler: EntityEventHandler): () => void;
    on(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void {
        if (typeof arg2 === 'string') {
            return this.#addHandler(this.#handlers, `${arg1}\0${arg2}`, arg3!);
        }
        return this.#addHandler(this.#globalHandlers, arg1, arg2);
    }

    off(event: string, handler: WorldEventHandler): void;
    off(entityId: string, event: string, handler: EntityEventHandler): void;
    off(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): void {
        if (typeof arg2 === 'string') {
            const handler = (this.#onceWrappers.get(arg3!) ?? arg3!) as EntityEventHandler;
            this.#handlers.get(`${arg1}\0${arg2}`)?.delete(handler);
            this.#onceWrappers.delete(arg3!);
        } else {
            const handler = (this.#onceWrappers.get(arg2) ?? arg2) as WorldEventHandler;
            this.#globalHandlers.get(arg1)?.delete(handler);
            this.#onceWrappers.delete(arg2);
        }
    }

    once(event: string, handler: WorldEventHandler): () => void;
    once(entityId: string, event: string, handler: EntityEventHandler): () => void;
    once(arg1: string, arg2: WorldEventHandler | string, arg3?: EntityEventHandler): () => void {
        if (typeof arg2 === 'string') {
            const original = arg3!;
            const wrapped: EntityEventHandler = data => { this.#onceWrappers.delete(original); unsub(); original(data); };
            this.#onceWrappers.set(original, wrapped);
            const unsub = this.on(arg1, arg2, wrapped);
            return () => { this.#onceWrappers.delete(original); unsub(); };
        }
        const original = arg2;
        const wrapped: WorldEventHandler = data => { this.#onceWrappers.delete(original); unsub(); original(data); };
        this.#onceWrappers.set(original, wrapped);
        const unsub = this.on(arg1, wrapped);
        return () => { this.#onceWrappers.delete(original); unsub(); };
    }

    #addHandler<T extends WorldEventHandler | EntityEventHandler>(
        map: Map<string, Set<T>>,
        key: string,
        handler: T,
    ): () => void {
        if (!map.has(key)) map.set(key, new Set());
        map.get(key)!.add(handler);
        return () => map.get(key)?.delete(handler);
    }

    *[Symbol.iterator]() {
        yield* this.#entities.values();
    }
}

export const isEvalContext = (x: unknown): x is EvalContext => (
    x instanceof Entity || x instanceof World
);