import { PassThrough } from 'stream'
import EventEmitter, { on } from 'events'

import { aiter } from 'iterator-helper'

import { chunk_position, same_chunk, chunk_index } from '../chunk.js'
import { Context, Mob } from '../events.js'

import { path_to_positions } from './path.js'

/** @param {import('../context.js').InitialWorldWithMobs} world */
export function register(world) {
  const mobs_positions = new EventEmitter()

  for (const mob of world.mobs.all) {
    const state = aiter(on(mob.events, Mob.STATE)).map(([state]) => state)

    const positions = path_to_positions(state)

    aiter(positions).reduce((last_position, { position, target }) => {
      if (last_position !== position) {
        const chunk_x = chunk_position(position.x)
        const chunk_z = chunk_position(position.z)

        const event = {
          mob,
          position,
          last_position,
          target,
          x: chunk_x,
          z: chunk_z,
        }

        mobs_positions.emit(chunk_index(chunk_x, chunk_z), event)
        mobs_positions.emit('*', event)

        if (last_position != null && !same_chunk(position, last_position)) {
          const last_chunk_x = chunk_position(last_position.x)
          const last_chunk_z = chunk_position(last_position.z)

          mobs_positions.emit(chunk_index(last_chunk_x, last_chunk_z), event)
        }
      }
      return position
    }, null)
  }

  return {
    ...world,
    mobs: {
      ...world.mobs,
      positions: mobs_positions,
    },
  }
}

export default function observe_world({ mobs }) {
  const actions = new PassThrough({ objectMode: true })

  mobs.positions.on('*', payload =>
    actions.write({ type: 'mob_position', payload })
  )

  /** @type {import('../context.js').Observer} */
  function observe({ events }) {
    events.on(Context.CHUNK_LOADED, ({ x, z, signal }) => {
      actions.write({
        type: 'client_chunk_loaded',
        payload: { events, x, z, signal },
      })
    })

    events.on(Context.CHUNK_UNLOADED, ({ x, z }) =>
      actions.write({
        type: 'client_chunk_unloaded',
        payload: { events, x, z },
      })
    )
  }

  aiter(actions).reduce(
    (mobs_by_chunk, { type, payload: { x, z, ...payload } }) => {
      const mobs = mobs_by_chunk.get(chunk_index(x, z)) ?? []

      if (type === 'client_chunk_loaded')
        payload.events.emit(Context.CHUNK_LOADED_WITH_MOBS, {
          mobs,
          x,
          z,
          signal: payload.signal,
        })
      else if (type === 'client_chunk_unloaded')
        payload.events.emit(Context.CHUNK_UNLOADED_WITH_MOBS, { mobs, x, z })
      else if (type === 'mob_position') {
        const { mob, last_position, position } = payload

        if (last_position == null)
          return new Map([
            ...mobs_by_chunk.entries(),
            [chunk_index(x, z), [...mobs, { mob, position }]],
          ])
        else if (!same_chunk(last_position, position)) {
          const last_x = chunk_position(last_position.x)
          const last_z = chunk_position(last_position.z)
          const last_mobs = mobs_by_chunk
            .get(chunk_index(last_x, last_z))
            .filter(({ mob: { entity_id } }) => entity_id !== mob.entity_id)

          return new Map([
            ...mobs_by_chunk.entries(),
            [chunk_index(last_x, last_z), last_mobs],
            [chunk_index(x, z), [...mobs, { mob, position }]],
          ])
        }
      } else throw new Error(`unknown type: ${type}`)

      return mobs_by_chunk
    },
    new Map()
  )

  return {
    observe,
  }
}