# encoding: UTF-8
# frozen_string_literal: true
require 'socket'
require_relative 'board'
require_relative 'move'
require_relative 'player'
require_relative 'network'
require_relative 'client_interface'
require 'rexml/document'
require 'rexml/streamlistener'
require 'builder'

# This class handles communication to the server over the XML communication
# protocol. Messages from the server are parsed and moves are serialized and
# send back.
class Protocol
  include Logging
  include REXML::StreamListener

  # @!attribute [r] gamestate
  # @return [Gamestate] current gamestate
  attr_reader :gamestate
  # @!attribute [rw] roomId
  # @return [String] current room id
  attr_accessor :roomId
  # @!attribute [r] client
  # @return [ClientInterface] current client
  attr_reader :client

  def initialize(network, client)
    @gamestate = GameState.new
    @network = network
    @client = client
    @context = {} # for saving context when stream-parsing the XML
    @client.gamestate = @gamestate
  end

  # starts xml-string parsing
  #
  # @param text [String] the xml-string that will be parsed
  def process_string(text)
    logger.debug "Parse XML:\n#{text}\n----END XML"
    REXML::Document.parse_stream(text, self)
  end


  # called when text is encountered
  def text(text)
    @context[:last_text] = text
  end

  # called if an end-tag is read
  #
  # @param name [String] the end-tag name, that was read
  def tag_end(name)
    case name
    when 'board'
      logger.debug @gamestate.board.to_s
    when 'type'
      @context[:player].cards << CardType.find_by_key(@context[:last_text].to_sym)
    end
  end

  # called if a start tag is read
  # Depending on the tag the gamestate is updated
  # or the client will be asked for a move
  #
  # @param name [String] the start-tag, that was read
  # @param attrs [Dictionary<String, String>] Attributes attached to the tag
  def tag_start(name, attrs)
    case name
    when 'room'
      @roomId = attrs['roomId']
      logger.info 'roomId : ' + @roomId
    when 'data'
      logger.debug "data(class) : #{attrs['class']}"
      @context[:data_class] = attrs['class']
      if attrs['class'] == 'sc.framework.plugins.protocol.MoveRequest'
        @client.gamestate = gamestate
        move = @client.move_requested
        sendString(move_to_xml(move))
      end
      if attrs['class'] == 'error'
        logger.info "Game ended - ERROR: #{attrs['message']}"
        @network.disconnect
      end
      if attrs['class'] == 'result'
        logger.info 'Got game result'
        @network.disconnect
      end
    when 'state'
      logger.debug 'new gamestate'
      @gamestate = GameState.new
      @gamestate.turn = attrs['turn'].to_i
      @gamestate.start_player_color = attrs['startPlayer'] == 'RED' ? PlayerColor::RED : PlayerColor::BLUE
      @gamestate.current_player_color = attrs['currentPlayer'] == 'RED' ? PlayerColor::RED : PlayerColor::BLUE
      logger.debug "Turn: #{@gamestate.turn}"
    when 'red'
      logger.debug 'new red player'
      player = parsePlayer(attrs)
      if player.color != PlayerColor::RED
        throw new IllegalArgumentException("expected #{PlayerColor::RED} Player but got #{player.color}")
      end
      @gamestate.add_player(player)
      @context[:player] = player
    when 'blue'
      logger.debug 'new blue player'
      player = parsePlayer(attrs)
      if player.color != PlayerColor::BLUE
        throw new IllegalArgumentException("expected #{PlayerColor::BLUE} Player but got #{player.color}")
      end
      @gamestate.add_player(player)
      @context[:player] = player
    when 'board'
      logger.debug 'new board'
      @gamestate.board = Board.new
      @context[:current_tile_index] = nil
      @context[:current_tile_direction] = nil
    when 'fields'
      type = FieldType.find_by_key(attrs['type'].to_sym)
      index = attrs['index'].to_i
      raise "unexpected field type: #{attrs['type']}. Known types are #{FieldType.map { |t| t.key.to_s }}" if type.nil?
      @gamestate.board.fields[index] = Field.new(type, index)
    when 'lastMove'
      @gamestate.last_move = Move.new
    when 'advance'
      @gamestate.last_move.add_action_with_order(
          Advance.new(attrs['distance'].to_i), attrs['order'].to_i
      )
    when 'card'
      @gamestate.last_move.add_action_with_order(
          Card.new(CardType.find_by_key(attrs['type'].to_sym), attrs['value'].to_i),
          attrs['order'].to_i
      )
    when 'skip'
      @gamestate.last_move.add_action_with_order(
          Skip.new, attrs['order'].to_i
      )
    when 'eatSalad'
      @gamestate.last_move.add_action_with_order(
          EatSalad.new, attrs['order'].to_i
      )
    when 'fallBack'
      @gamestate.last_move.add_action_with_order(
          FallBack.new, attrs['order'].to_i
      )
    when 'exchangeCarrots'
      @gamestate.last_move.add_action_with_order(
          ExchangeCarrots.new(attrs['value'].to_i), attrs['order'].to_i
      )
    when 'winner'
      winning_player = parsePlayer(attrs)
      @gamestate.condition = Condition.new(winning_player)
      @context[:player] = winning_player
    when 'left'
      logger.debug 'got left event, terminating'
      @network.disconnect
    when 'lastNonSkipAction'
      @context[:player].last_non_skip_action =
        case attrs['class']
        when 'advance'
          Advance.new(attrs['distance'].to_i)
        when 'card'
          Card.new(CardType.find_by_key(attrs['type'].to_sym), attrs['value'].to_i)
        when 'skip'
          Skip.new
        when 'eatSalad'
          EatSalad.new
        when 'fallBack'
          FallBack.new
        when 'exchangeCarrots'
          ExchangeCarrots.new(attrs['value'].to_i)
        else
          raise "Unknown action type #{attrs['class']}"
        end
    end
  end

  # Converts XML attributes for a Player to a new Player object
  #
  # @param attributes [Hash] Attributes for the new Player.
  # @return [Player] The created Player object.
  def parsePlayer(attributes)
    player = Player.new(
      PlayerColor.find_by_key(attributes['color'].to_sym),
      attributes['displayName']
    )
    player.points = attributes['points'].to_i
    player.index = attributes['index'].to_i
    player.carrots = attributes['carrots'].to_i
    player.salads = attributes['salads'].to_i
    player.cards = []
    player
  end

  # send a xml document
  #
  # @param document [REXML::Document] the document, that will be send to the connected server
  def sendXml(document)
    @network.sendXML(document)
  end

  # send a string
  #
  # @param document [String] The string that will be send to the connected server.
  def sendString(string)
    @network.sendString("<room roomId=\"#{@roomId}\">#{string}</room>")
  end

  # converts "this_snake_case" to "thisSnakeCase"
  def snake_case_to_lower_camel_case(string)
    string.split('_').inject([]) do |result, e|
      result + [result.empty? ? e : e.capitalize]
    end.join
  end

  # Converts a move to XML for sending to the server.
  #
  # @param move [Move] The move to convert to XML.
  def move_to_xml(move)
    builder = Builder::XmlMarkup.new(indent: 2)
    builder.data(class: 'move') do |data|
      move.actions.each_with_index do |action, index|
        # Converting every action type here instead of requiring the Action
        # class interface to supply a method which returns the action hash
        # because XML-generation should be decoupled from internal data
        # structures.
        attribute = case action.type
                    when :advance
                      { distance: action.distance }
                    when :skip, :eat_salad, :fall_back
                      {}
                    when :card
                      { type: action.card_type.key.to_s, value: action.value }
                    when :exchange_carrots
                      { value: action.value }
                    else
                      raise "unknown action type: #{action.type.inspect}. "\
                            "Can't convert to XML!"
                    end
        attribute[:order] = index
        data.tag!(snake_case_to_lower_camel_case(action.type.to_s), attribute)
      end
    end
    move.hints.each do |hint|
      data.hint(content: hint.content)
    end
    builder.target!
  end

end