import socket, struct, thread, time

class HFO_Features:
  ''' An enum of the possible HFO feature sets. For descriptions see
  https://github.com/mhauskn/HFO/blob/master/doc/manual.pdf
  '''
  LOW_LEVEL_FEATURE_SET, HIGH_LEVEL_FEATURE_SET = range(2)


class HFO_Actions:
  ''' An enum of the possible HFO actions

  [Low-Level] Dash(power, relative_direction)
  [Low-Level] Turn(direction)
  [Low-Level] Tackle(direction)
  [Low-Level] Kick(power, direction)
  [Mid-Level] Kick_To(target_x, target_y, speed)
  [Mid-Level] Move(target_x, target_y)
  [Mid-Level] Dribble(target_x, target_y)
  [Mid-Level] Intercept(): Intercept the ball
  [High-Level] Move(): Reposition player according to strategy
  [High-Level] Shoot(): Shoot the ball
  [High-Level] Pass(teammate_unum): Pass to teammate
  [High-Level] Dribble(): Offensive dribble
  [High-Level] Catch(): Catch the ball (Goalie Only)
  NOOP(): Do Nothing
  QUIT(): Quit the game

  '''
  DASH, TURN, TACKLE, KICK, KICK_TO, MOVE_TO, DRIBBLE_TO, INTERCEPT, \
    MOVE, SHOOT, PASS, DRIBBLE, CATCH, NOOP, QUIT = range(15)

class HFO_Status:
  ''' Current status of the HFO game. '''
  IN_GAME, GOAL, CAPTURED_BY_DEFENSE, OUT_OF_BOUNDS, OUT_OF_TIME = range(5)


class HFOEnvironment(object):
  ''' The HFOEnvironment is designed to be the main point of contact
  between a learning agent and the Half-Field-Offense domain.

  '''
  def __init__(self):
    self.socket = None           # Socket connection to server
    self.numFeatures = None      # Given by the server in handshake
    self.features = None         # The state features
    self.requested_action = None # Action to execute and parameters
    self.say_msg = None          # Outgoing message to say
    self.hear_msg = None         # Incoming heard message

  def NumParams(self, action_type):
    ''' Returns the number of required parameters for each action type. '''
    return {
      HFO_Actions.DASH : 2,
      HFO_Actions.TURN : 1,
      HFO_Actions.TACKLE : 1,
      HFO_Actions.KICK : 2,
      HFO_Actions.KICK_TO : 3,
      HFO_Actions.MOVE_TO : 2,
      HFO_Actions.DRIBBLE_TO : 2,
      HFO_Actions.INTERCEPT : 0,
      HFO_Actions.MOVE : 0,
      HFO_Actions.SHOOT : 0,
      HFO_Actions.PASS : 1,
      HFO_Actions.DRIBBLE : 0,
      HFO_Actions.CATCH : 0,
      HFO_Actions.NOOP : 0,
      HFO_Actions.QUIT : 0}.get(action_type, -1);

  def connectToAgentServer(self, server_port=6000,
                           feature_set=HFO_Features.HIGH_LEVEL_FEATURE_SET):
    '''Connect to the server that controls the agent on the specified port. '''
    self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print '[Agent Client] Connecting to Agent Server on port', server_port
    retry = 10
    while retry > 0:
      try:
        self.socket.connect(('localhost', server_port))
      except:
        time.sleep(1)
        retry -= 1
        continue
      else:
        break
    if retry <= 0:
      print '[Agent Client] ERROR Unable to communicate with server'
      exit(1)
    print '[Agent Client] Connected'
    self.handshakeAgentServer(feature_set)
    # Get the initial state
    state_data = self.socket.recv(struct.calcsize('f')*self.numFeatures)
    if not state_data:
      print '[Agent Client] ERROR Recieved bad data from Server. Perhaps server closed?'
      self.cleanup()
      exit(1)
    self.features = struct.unpack('f'*self.numFeatures, state_data)

  def handshakeAgentServer(self, feature_set):
    '''Handshake with the agent's server. '''
    # Recieve float 123.2345
    data = self.socket.recv(struct.calcsize("f"))
    f = struct.unpack("f", data)[0]
    assert abs(f - 123.2345) < 1e-4, "Float handshake failed"
    # Send float 5432.321
    self.socket.send(struct.pack("f", 5432.321))
    # Send the feature set request
    self.socket.send(struct.pack("i", feature_set))
    # Recieve the number of features
    data = self.socket.recv(struct.calcsize("i"))
    self.numFeatures = struct.unpack("i", data)[0]
    # Send what we recieved
    self.socket.send(struct.pack("i", self.numFeatures))
    # Get the current game status
    data = self.socket.recv(struct.calcsize("i"))
    status = struct.unpack("i", data)[0]
    assert status == HFO_Status.IN_GAME, "Status check failed"
    print '[Agent Client] Handshake complete'

  def getState(self):
    '''Get the current state of the world. Returns a list of floats with
    size numFeatures. '''
    return self.features

  def act(self, *args):
    ''' Send an action and recieve the game status.'''
    assert len(args) > 0, 'Not enough arguments provided to act'
    action_type = args[0]
    n_params = self.NumParams(action_type)
    assert n_params == len(args) - 1, 'Incorrect number of params to act: '\
      'Required %d provided %d'%(n_params, len(args)-1)
    self.requested_action = args

  def say(self, message):
    ''' Send a communication message to other agents. '''
    self.say_msg = message

  def hear(self):
    ''' Receive incoming communications from other players. '''
    return self.hear_msg

  def step(self):
    ''' Indicates the agent is done and the environment should
        progress. Returns the game status after the step'''
    # Send action and parameters
    self.socket.send(struct.pack('i'+'f'*(len(self.requested_action)-1),
                                 *self.requested_action))
    # TODO: [Sanmit] Send self.say_msg
    self.say_msg = ''
    # Get the current game status
    data = self.socket.recv(struct.calcsize("i"))
    status = struct.unpack("i", data)[0]
    # Get the next state features
    state_data = self.socket.recv(struct.calcsize('f')*self.numFeatures)
    if not state_data:
      print '[Agent Client] ERROR Recieved bad data from Server. Perhaps server closed?'
      self.cleanup()
      exit(1)
    self.features = struct.unpack('f'*self.numFeatures, state_data)
    self.hear_msg = ''
    # TODO: [Sanmit] Receive self.hear_msg
    return status

  def cleanup(self):
    ''' Send a quit and close the connection to the agent's server. '''
    self.socket.send(struct.pack("i", HFO_Actions.QUIT))
    self.socket.close()