import { World, Entity, Component, WORLD_ENTITY_COUNTER, COMPONENT_KEY } from './world';
import { getComponentMeta, getComponentName, migrateState } from '../utils/decorators';

/** Increment this when the WorldData/EntityData structure itself changes incompatibly. */
const SCHEMA_VERSION = 1;

interface ComponentData {
    type: 'component';
    name: string;
    key: string | null;  // null = was symbol-keyed (anonymous)
    version: number;
    state: unknown;
}

interface EntityData {
    type: 'entity';
    id: string;
    components: ComponentData[];
}

interface WorldData {
    type: 'world';
    schemaVersion: number;
    globals: Record<string, string | number | boolean | undefined>;
    entityCounter: number;
    entities: EntityData[];
}

type AnyData = ComponentData | EntityData | WorldData;

function serializeComponent(component: Component<any>): ComponentData {
    const name = getComponentName(component.constructor);
    if (!name) {
        throw new Error(
            `Component '${component.constructor.name}' is not registered. ` +
            `Add @component to the class declaration.`
        );
    }
    const meta = getComponentMeta(name)!;
    const key = typeof component[COMPONENT_KEY] === 'symbol' ? null : component.key;
    return { type: 'component', name, key, version: meta.version, state: component.state };
}

function serializeEntity(entity: Entity): EntityData {
    const components: ComponentData[] = [];
    for (const [, component] of entity) {
        components.push(serializeComponent(component));
    }
    return { type: 'entity', id: entity.id, components };
}

function serializeWorld(world: World): WorldData {
    const entities: EntityData[] = [];
    for (const entity of world) {
        entities.push(serializeEntity(entity));
    }
    return {
        type: 'world',
        schemaVersion: SCHEMA_VERSION,
        globals: { ...world.globals },
        entityCounter: world[WORLD_ENTITY_COUNTER],
        entities,
    };
}

function deserializeComponent(data: ComponentData): Component<any> {
    const meta = getComponentMeta(data.name);
    if (!meta) {
        throw new Error(`Unknown component '${data.name}'. Ensure it is imported so @component runs.`);
    }

    const savedVersion = data.version ?? 0;
    const state = savedVersion < meta.version
        ? migrateState(data.name, data.state as Record<string, unknown>, savedVersion)
        : data.state;

    // Bypass constructor: create a bare instance and restore state directly.
    // Safe because constructors must only call super(state) — all initialization
    // logic goes in onAdd(), which entity.add() calls after this.
    const instance = Object.create(meta.ctor.prototype) as Component<any>;
    (instance as unknown as { state: unknown }).state = state;
    return instance;
}

function deserializeEntity(data: EntityData, world: World): Entity {
    const entity = world.createEntity(data.id);
    for (const componentData of data.components) {
        const component = deserializeComponent(componentData);
        if (componentData.key === null) {
            entity.add(component);
        } else {
            entity.add(component, componentData.key);
        }
    }
    return entity;
}

function deserializeWorld(data: WorldData): World {
    if (data.schemaVersion !== SCHEMA_VERSION) {
        throw new Error(
            `Unsupported save format: schema version ${data.schemaVersion}, ` +
            `expected ${SCHEMA_VERSION}. The save file is incompatible with this version of the engine.`
        );
    }
    const world = new World();
    Object.assign(world.globals, data.globals);
    world[WORLD_ENTITY_COUNTER] = data.entityCounter;
    for (const entityData of data.entities) {
        deserializeEntity(entityData, world);
    }
    return world;
}

export namespace Serialization {
    export function serialize(x: World | Entity | Component<any>): string {
        if (x instanceof World) return JSON.stringify(serializeWorld(x));
        if (x instanceof Entity) return JSON.stringify(serializeEntity(x));
        return JSON.stringify(serializeComponent(x));
    }

    export function deserialize(s: string): World | Entity | Component<any> {
        const data = JSON.parse(s) as AnyData;
        switch (data.type) {
            case 'world': return deserializeWorld(data);
            case 'entity': {
                const world = new World();
                return deserializeEntity(data, world);
            }
            case 'component': return deserializeComponent(data);
            default: throw new Error(`Unknown serialized type: '${(data as AnyData).type}'`);
        }
    }
}
