defmodule ThistleTea.Game do
  use ThousandIsland.Handler

  import Bitwise, only: [|||: 2]
  import ThistleTea.Game.Combat, only: [handle_attack_swing: 1]
  import ThistleTea.Game.Logout, only: [handle_logout: 1]
  import ThistleTea.Game.Spell, only: [handle_spell_complete: 1]

  import ThistleTea.Util,
    only: [send_packet: 2, send_update_packet: 1, parse_string: 1]

  alias ThistleTea.CryptoStorage
  alias ThistleTea.Game.Character
  alias ThistleTea.Game.Chat
  alias ThistleTea.Game.Combat
  alias ThistleTea.Game.Entity
  alias ThistleTea.Game.Gossip
  alias ThistleTea.Game.Login
  alias ThistleTea.Game.Logout
  alias ThistleTea.Game.Movement
  alias ThistleTea.Game.Ping
  alias ThistleTea.Game.Query
  alias ThistleTea.Game.Spell
  alias ThistleTea.Game.Utils.UpdateObject

  require Logger

  @smsg_auth_challenge 0x1EC
  @cmsg_auth_session 0x1ED
  @smsg_auth_response 0x1EE
  @smsg_pong 0x1DD

  @smsg_transfer_pending 0x03F
  @smsg_new_world 0x03E

  @smsg_destroy_object 0x0AA

  @smsg_logout_complete 0x04D

  @cmsg_char_enum 0x037
  @cmsg_char_create 0x036

  @character_opcodes [
    @cmsg_char_enum,
    @cmsg_char_create
  ]

  @cmsg_messagechat 0x095
  @cmsg_join_channel 0x097
  @cmsg_leave_channel 0x098
  @cmsg_text_emote 0x104

  @chat_opcodes [
    @cmsg_messagechat,
    @cmsg_join_channel,
    @cmsg_leave_channel,
    @cmsg_text_emote
  ]

  @cmsg_attackswing 0x141
  @cmsg_attackstop 0x142
  @cmsg_setsheathed 0x1E0
  @cmsg_set_selection 0x13D

  @combat_opcodes [
    @cmsg_attackswing,
    @cmsg_attackstop,
    @cmsg_setsheathed,
    @cmsg_set_selection
  ]

  @cmsg_gossip_hello 0x17B
  @cmsg_gossip_select_option 0x17C
  @cmsg_npc_text_query 0x17F

  @gossip_opcodes [
    @cmsg_gossip_hello,
    @cmsg_gossip_select_option,
    @cmsg_npc_text_query
  ]

  @cmsg_player_login 0x03D
  @msg_move_worldport_ack 0x0DC

  @login_opcodes [
    @cmsg_player_login,
    @msg_move_worldport_ack
  ]

  @cmsg_logout_request 0x04B
  @cmsg_logout_cancel 0x04E

  @logout_opcodes [
    @cmsg_logout_request,
    @cmsg_logout_cancel
  ]

  @msg_move_start_forward 0x0B5
  @msg_move_start_backward 0x0B6
  @msg_move_stop 0x0B7
  @msg_move_start_strafe_left 0x0B8
  @msg_move_start_strafe_right 0x0B9
  @msg_move_stop_strafe 0x0BA
  @msg_move_jump 0x0BB
  @msg_move_start_turn_left 0x0BC
  @msg_move_start_turn_right 0x0BD
  @msg_move_stop_turn 0x0BE
  @msg_move_start_pitch_up 0x0BF
  @msg_move_start_pitch_down 0x0C0
  @msg_move_stop_pitch 0x0C1
  @msg_move_set_run_mode 0x0C2
  @msg_move_set_walk_mode 0x0C3
  @msg_move_fall_land 0x0C9
  @msg_move_start_swim 0x0CA
  @msg_move_stop_swim 0x0CB
  @msg_move_set_facing 0x0DA
  @msg_move_set_pitch 0x0DB
  @msg_move_heartbeat 0x0EE
  @cmsg_move_fall_reset 0x2CA
  @cmsg_standstatechange 0x101

  @movement_opcodes [
    @msg_move_start_forward,
    @msg_move_start_backward,
    @msg_move_stop,
    @msg_move_start_strafe_left,
    @msg_move_start_strafe_right,
    @msg_move_stop_strafe,
    @msg_move_jump,
    @msg_move_start_turn_left,
    @msg_move_start_turn_right,
    @msg_move_stop_turn,
    @msg_move_start_pitch_up,
    @msg_move_start_pitch_down,
    @msg_move_stop_pitch,
    @msg_move_set_run_mode,
    @msg_move_set_walk_mode,
    @msg_move_fall_land,
    @msg_move_start_swim,
    @msg_move_stop_swim,
    @msg_move_set_facing,
    @msg_move_set_pitch,
    @msg_move_heartbeat,
    @cmsg_move_fall_reset,
    @cmsg_standstatechange
  ]

  @cmsg_ping 0x1DC

  @ping_opcodes [
    @cmsg_ping
  ]

  @cmsg_name_query 0x050
  @cmsg_item_query_single 0x056
  @cmsg_item_name_query 0x2C4
  @cmsg_gameobject_query 0x05E
  @cmsg_creature_query 0x060
  @cmsg_who 0x62

  @query_opcodes [
    @cmsg_name_query,
    @cmsg_item_query_single,
    @cmsg_item_name_query,
    @cmsg_gameobject_query,
    @cmsg_creature_query,
    @cmsg_who
  ]

  @cmsg_cast_spell 0x12E
  @cmsg_cancel_cast 0x12F

  @spell_opcodes [
    @cmsg_cast_spell,
    @cmsg_cancel_cast
  ]

  @update_flag_high_guid 0x08
  @update_flag_living 0x20
  @update_flag_has_position 0x40

  @smsg_new_world 0x03E

  def dispatch_packet(opcode, payload, state) when opcode in @character_opcodes do
    Character.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @chat_opcodes do
    Chat.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @combat_opcodes do
    Combat.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @gossip_opcodes do
    Gossip.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @login_opcodes do
    Login.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @logout_opcodes do
    Logout.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @movement_opcodes do
    Movement.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @ping_opcodes do
    Ping.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @query_opcodes do
    Query.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, payload, state) when opcode in @spell_opcodes do
    Spell.handle_packet(opcode, payload, state)
  end

  def dispatch_packet(opcode, _payload, state) do
    Logger.error("UNIMPLEMENTED: #{ThistleTea.Opcodes.get(opcode)}")
    {:continue, state}
  end

  def handle_additional_data(data, socket, state) do
    if byte_size(data) > 0 do
      handle_data(data, socket, state)
    else
      {:continue, state}
    end
  end

  @impl ThousandIsland.Handler
  def handle_data(
        <<size::big-size(16), @cmsg_auth_session::little-size(32), body::binary-size(size - 4),
          additional_data::binary>>,
        socket,
        state
      ) do
    <<_build::little-size(32), _server_id::little-size(32), rest::binary>> = body
    {:ok, username, rest} = parse_string(rest)
    Logger.metadata(username: username)
    Logger.info("CMSG_AUTH_SESSION")

    <<client_seed::little-bytes-size(4), client_proof::little-bytes-size(20), _rest::binary>> =
      rest

    [{^username, session}] = :ets.lookup(:session, username)

    server_proof =
      :crypto.hash(
        :sha,
        username <> <<0::little-size(32)>> <> client_seed <> state.seed <> session
      )

    if client_proof == server_proof do
      Logger.info("AUTH SUCCESS")
      crypt = %{key: session, send_i: 0, send_j: 0, recv_i: 0, recv_j: 0}
      {:ok, crypto_pid} = CryptoStorage.start_link(crypt)
      {:ok, account} = ThistleTea.Account.get_user(username)
      send_packet(@smsg_auth_response, <<0x0C, 0::little-size(32), 0, 0::little-size(32)>>)

      state = Map.merge(state, %{crypto_pid: crypto_pid, account: account})
      handle_additional_data(additional_data, socket, state)
    else
      Logger.error("AUTH FAILURE")
      {:close, state}
    end
  end

  @impl ThousandIsland.Handler
  def handle_data(
        <<size::big-size(16), @cmsg_ping::little-size(32), body::binary-size(size - 4), additional_data::binary>>,
        socket,
        state
      ) do
    <<sequence_id::little-size(32), latency::little-size(32)>> = body

    Logger.info("CMSG_PING latency=#{latency}")

    ThousandIsland.Socket.send(
      socket,
      <<6::big-size(16), @smsg_pong::little-size(16), sequence_id::little-size(32)>>
    )

    state = Map.put(state, :latency, latency)
    handle_additional_data(additional_data, socket, state)
  end

  @impl ThousandIsland.Handler
  def handle_data(data, socket, state) do
    state = Map.put(state, :packet_stream, Map.get(state, :packet_stream, <<>>) <> data)
    handle_packet(socket, state)
  end

  def handle_packet(socket, %{packet_stream: <<header::bytes-size(6), rest::binary>>} = state) do
    case CryptoStorage.decrypt_header(state.crypto_pid, header, byte_size(rest)) do
      {:ok, decrypted_header} ->
        <<size::big-size(16), opcode::little-size(32)>> = decrypted_header
        <<payload::binary-size(size - 4), rest::binary>> = rest
        state = Map.delete(state, :packet_stream)

        {action, state} =
          :telemetry.span([:thistle_tea, :handle_packet], %{opcode: opcode}, fn ->
            {action, state} = dispatch_packet(opcode, payload, state)
            {{action, state}, %{opcode: opcode}}
          end)

        if action == :continue do
          handle_additional_data(rest, socket, state)
        else
          {action, state}
        end

      {:error, _} ->
        {:continue, state}
    end
  end

  # accumulate packet_stream until enough data
  def handle_packet(_socket, state), do: {:continue, state}

  @impl GenServer
  def handle_cast({:send_packet, opcode, payload}, {socket, state}) do
    {:ok, header} = CryptoStorage.encrypt_header(state.crypto_pid, opcode, payload)
    ThousandIsland.Socket.send(socket, header <> payload)
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_cast({:send_update_packet, packet}, {socket, state}) do
    send_update_packet(packet)
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_cast({:send_update_to, pid}, {socket, state}) do
    packet =
      state.character
      |> ThistleTea.Character.get_update_fields()
      |> Map.merge(%{
        update_type: :create_object2,
        object_type: :player,
        movement_block:
          Map.put(
            state.character.movement,
            :update_flag,
            @update_flag_high_guid ||| @update_flag_living ||| @update_flag_has_position
          )
      })
      |> UpdateObject.to_packet()

    GenServer.cast(pid, {:send_update_packet, packet})
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_cast({:destroy_object, guid}, {socket, state}) do
    send_packet(@smsg_destroy_object, <<guid::little-size(64)>>)
    # TODO: remove from spawned_players/etc.?
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_cast({:start_teleport, x, y, z, map}, {socket, state}) do
    # Send player's client to loading screen to load the new map
    transfer_pending_packet = <<map::little-size(32)>>
    send_packet(@smsg_transfer_pending, transfer_pending_packet)

    # Update player's location
    area =
      case ThistleTea.Pathfinding.get_zone_and_area(map, {x, y, z}) do
        {_zone, area} -> area
        nil -> state.character.area
      end

    character = state.character

    character = %{
      character
      | area: area,
        map: map,
        movement: %{character.movement | position: {x, y, z, 0.0}}
    }

    SpatialHash.update(
      :players,
      state.guid,
      self(),
      character.map,
      x,
      y,
      z
    )

    state =
      Map.merge(state, %{
        character: character,
        ready: false
      })
      |> Map.drop([:spawned_players, :spawned_mobs, :spawned_game_objects])

    # Send player's client the new location
    orientation = 0

    new_world_packet = <<
      map::little-size(32),
      x::little-float-size(32),
      y::little-float-size(32),
      z::little-float-size(32),
      orientation::little-float-size(32)
    >>

    send_packet(@smsg_new_world, new_world_packet)

    # The client responds with a MSG_MOVE_WORLDPORT_ACK message which
    # is handled in the login handler as they share the same init process
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_info(:logout_complete, {socket, state}) do
    send_packet(@smsg_logout_complete, <<>>)
    state = handle_logout(state)
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_info(:spell_complete, {socket, state}) do
    state = handle_spell_complete(state)
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_info(:attack_swing, {socket, state}) do
    state = handle_attack_swing(state)
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_info(:spawn_objects, {socket, %{character: c, ready: true} = state}) do
    # TODO: add telemetry for periodic tasks
    {x, y, z, _o} = c.movement.position

    old_players = Map.get(state, :spawned_players, MapSet.new())
    old_mobs = Map.get(state, :spawned_mobs, MapSet.new())
    old_game_objects = Map.get(state, :spawned_game_objects, MapSet.new())

    new_players =
      SpatialHash.query(:players, c.map, x, y, z, 250)
      |> MapSet.new(fn {guid, pid, _distance} -> {guid, pid} end)

    new_mobs =
      SpatialHash.query(:mobs, c.map, x, y, z, 250)
      |> MapSet.new(fn {guid, pid, _distance} -> {guid, pid} end)

    new_game_objects =
      SpatialHash.query(:game_objects, c.map, x, y, z, 250)
      |> MapSet.new(fn {guid, pid, _distance} -> {guid, pid} end)

    players_to_remove = MapSet.difference(old_players, new_players)
    mobs_to_remove = MapSet.difference(old_mobs, new_mobs)
    game_objects_to_remove = MapSet.difference(old_game_objects, new_game_objects)

    players_to_add = MapSet.difference(new_players, old_players)
    mobs_to_add = MapSet.difference(new_mobs, old_mobs)
    game_objects_to_add = MapSet.difference(new_game_objects, old_game_objects)

    # TODO: update a reverse mapping, so mobs know nearby players?
    for {guid, pid} <- players_to_remove do
      if pid != self() do
        send_packet(@smsg_destroy_object, <<guid::little-size(64)>>)
      end
    end

    for {guid, _pid} <- mobs_to_remove do
      send_packet(@smsg_destroy_object, <<guid::little-size(64)>>)
    end

    for {guid, _pid} <- game_objects_to_remove do
      send_packet(@smsg_destroy_object, <<guid::little-size(64)>>)
    end

    for {_guid, pid} <- players_to_add do
      if pid != self() do
        Entity.request_update_from(pid)
      end
    end

    for {_guid, pid} <- mobs_to_add do
      Entity.request_update_from(pid)
    end

    for {_guid, pid} <- game_objects_to_add do
      Entity.request_update_from(pid)
    end

    # TODO: redundant, refactor out?
    player_pids = new_players |> Enum.map(fn {_guid, pid} -> pid end)
    mob_pids = new_mobs |> Enum.map(fn {_guid, pid} -> pid end)

    state =
      Map.merge(state, %{
        spawned_players: new_players,
        spawned_mobs: new_mobs,
        spawned_game_objects: new_game_objects,
        player_pids: player_pids,
        mob_pids: mob_pids
      })

    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_info(:spawn_objects, {socket, state}) do
    {:noreply, {socket, state}, socket.read_timeout}
  end

  @impl GenServer
  def handle_call(:get_entity, _from, state) do
    {:reply, :player, state}
  end

  @impl GenServer
  def handle_call(:get_name, _from, state) do
    {_socket, s} = state
    {:reply, s.character.name, state}
  end

  @impl ThousandIsland.Handler
  def handle_connection(socket, _state) do
    Logger.info("SMSG_AUTH_CHALLENGE")
    seed = :crypto.strong_rand_bytes(4)

    ThousandIsland.Socket.send(
      socket,
      <<6::big-size(16), @smsg_auth_challenge::little-size(16)>> <> seed
    )

    {:continue, %{seed: seed}}
  end

  @impl ThousandIsland.Handler
  def handle_close(_socket, state) do
    Logger.info("CLIENT DISCONNECTED")
    handle_logout(state)
  end
end
