# Balazar
# Copyright (C) 2003-2005 Jean-Baptiste LAMY
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import math, random, types
import tofu, soya, soya.tofu4soya, soya.sdlconst as sdlconst
import balazar
from balazar.character import *
from balazar.character import _P, _V


# constants of action detected on keyboard/joystick
CONTROL_UNKNOWN       =  0
CONTROL_UP            =  1
CONTROL_DOWN          =  2
CONTROL_LEFT          =  3
CONTROL_RIGHT         =  4
CONTROL_JUMP          =  5
CONTROL_STRIKE        =  6
CONTROL_ITEM          =  7
CONTROL_QUIT          =  8
CONTROL_PAUSE         =  9
CONTROL_MENU          = 10
CONTROL_LOOK_AT_LEFT  = 11
CONTROL_LOOK_AT_RIGHT = 12
CONTROL_SUICIDE       = 13
CONTROL_SCREENSHOT    = 14

class Controls:
  def __init__(self):
    self.control_names = {
      CONTROL_UP           : "Advance",
      CONTROL_DOWN         : "Go back",
      CONTROL_LEFT         : "Turn left",
      CONTROL_RIGHT        : "Turn right",
      CONTROL_JUMP         : "Jump",
      CONTROL_STRIKE       : "Strike",
      CONTROL_ITEM         : "Use item",
      CONTROL_QUIT         : "Quit",
      CONTROL_PAUSE        : "Pause",
      CONTROL_MENU         : "Menu / inventory",
      CONTROL_LOOK_AT_LEFT : "Look at left",
      CONTROL_LOOK_AT_RIGHT: "Look at right",
      CONTROL_SUICIDE      : "Suicide",
      CONTROL_SCREENSHOT   : "Take screenshot",
      }
    self.control_2_events = {
      CONTROL_UP           : [sdlconst.K_UP   ],
      CONTROL_DOWN         : [sdlconst.K_DOWN ],
      CONTROL_LEFT         : [sdlconst.K_LEFT ],
      CONTROL_RIGHT        : [sdlconst.K_RIGHT],
      CONTROL_JUMP         : [sdlconst.K_LSHIFT, sdlconst.K_RSHIFT , sdlconst.K_0],
      CONTROL_STRIKE       : [sdlconst.K_LCTRL , sdlconst.K_RCTRL  , sdlconst.K_1],
      CONTROL_ITEM         : [sdlconst.K_LALT  , sdlconst.K_RALT   , sdlconst.K_DELETE, sdlconst.K_4],
      CONTROL_QUIT         : [sdlconst.K_q     , sdlconst.K_ESCAPE , sdlconst.K_3],
      CONTROL_PAUSE        : [sdlconst.K_p     , sdlconst.K_5],
      CONTROL_MENU         : [sdlconst.K_RETURN, sdlconst.K_2],
      CONTROL_LOOK_AT_LEFT : [sdlconst.K_w     , sdlconst.K_6],
      CONTROL_LOOK_AT_RIGHT: [sdlconst.K_x     , sdlconst.K_7],
      CONTROL_SUICIDE      : [sdlconst.K_k],
      CONTROL_SCREENSHOT   : [sdlconst.K_c],
      }
    self.changed()
    
  def changed(self):
    self.event_2_control = dict([(event, control) for (control, events) in self.control_2_events.items() for event in events])
    
  def assign_event_to_control(self, event, control):
    # First, remove the event (if it was previously linked to another control)
    if self.event_2_control.has_key(event):
      self.control_2_events[self.event_2_control[event]].remove(event)
    self.control_2_events[control].append(event)
    self.changed()
    
  def get_events_for_control(self, control): return self.control_2_events[control]
  
  def get_control_for_event(self, event): return self.event_2_control.get(event, CONTROL_UNKNOWN)
  
  def get_control(self, sdl_event):
    if   (sdl_event[0] == sdlconst.KEYDOWN) or (sdl_event[0] == sdlconst.KEYUP):
      return self.event_2_control.get(sdl_event[1], CONTROL_UNKNOWN)
    
    elif (sdl_event[0] == sdlconst.JOYBUTTONDOWN) or (sdl_event[0] == sdlconst.JOYBUTTONUP):
      # HACK ! Joystick button #n is considered as the same as key "n"
      return self.event_2_control.get(sdlconst.K_0 + sdl_event[1], CONTROL_UNKNOWN)
    
    elif sdl_event[0] == sdlconst.JOYAXISMOTION:
      if   sdl_event[1] == 0:
        if   sdl_event[2] < 0: return CONTROL_LEFT
        elif sdl_event[2] > 0: return CONTROL_RIGHT
      elif sdl_event[1] == 1:
        if   sdl_event[2] < 0: return CONTROL_UP
        elif sdl_event[2] > 0: return CONTROL_DOWN
        
    return CONTROL_UNKNOWN

  def control_label(self, control):
    return u"%s : %s" % (self.control_name(control), u", ".join([self.event_name(event) for event in self.get_events_for_control(control)]))
    
  def controls(self):
    return CONTROLS.control_2_events.keys()
    
  def control_name(self, control):
    return _(self.control_names[control])
    
  def event_name(self, event):
    const_2_name = dict([(value, name) for (name, value) in sdlconst.__dict__.items()])
    return _(const_2_name[event][2:].lower())
  
CONTROLS = Controls()


class StackController(soya.tofu4soya.LocalController):
  def __init__(self, mobile, controllers  = None):
    soya.tofu4soya.LocalController.__init__(self, mobile)
    self.controllers = controllers or []
    self.uncancelable_controller = None
    
  def __getstate__(self):
    state = self.__dict__.copy()
    state["controllers"            ] = []
    state["uncancelable_controller"] = None
    return state
  
  def animate(self, controller):
    if not self.uncancelable_controller in self.controllers:
      self.controllers.append(controller)
      self.uncancelable_controller = controller
      return 1
    
  def animate_and_cancel(self, controller):
    self.controllers *= 0
    self.controllers.append(controller)
    self.uncancelable_controller = controller
    return 1
  
  def append(self, controller):
    if not self.uncancelable_controller in self.controllers:
      self.controllers.append(controller)
      
  def append_and_cancel(self, controller):
    if not self.uncancelable_controller in self.controllers:
      self.controllers *= 0
      self.controllers.append(controller)
      
  def cancel(self, controller):
    try: self.controllers.remove(controller)
    except: return 0
    if self.uncancelable_controller is controller: self.uncancelable_controller = None
    return 1
  
  def big_round(self): pass
  
  def begin_round(self):
    while self.controllers:
      action = self.controllers[-1]
      if   action is None:
        self.mobile.doer.do_action(None)
        return
      
      elif isinstance(action, tofu.Action):
        del self.controllers[-1]
        self.mobile.doer.do_action(action)
        return
      
      elif isinstance(action, soya.tofu4soya.LocalController):
        r = action.begin_round()
        if r: return
        else: del self.controllers[-1]
          
      else: # A generator / a pseudo-generator
        try: action = action.next()
        except StopIteration:
          if self.uncancelable_controller is self.controllers[-1]: self.uncancelable_controller = None
          del self.controllers[-1]
        else:
          if   action is None:
            self.mobile.doer.do_action(None)
            return
          
          elif isinstance(action, tofu.Action):
            self.mobile.doer.do_action(action)
            return
          
          elif isinstance(action, types.GeneratorType): self.controllers.append(action)
          
  def relation_changed(self, character): pass
  
  
GUARD_CONTROL_2_ACTION = {
  (1, 0) : ACTION_TURN_LEFT,
  (0, 1) : ACTION_TURN_RIGHT,
  }

NORMAL_CONTROL_2_ACTION = {
  (0, 0, 1, 0) : ACTION_ADVANCE,
  (1, 0, 1, 0) : ACTION_ADVANCE_LEFT,
  (0, 1, 1, 0) : ACTION_ADVANCE_RIGHT,
  (1, 0, 0, 0) : ACTION_TURN_LEFT,
  (0, 1, 0, 0) : ACTION_TURN_RIGHT,
  (0, 0, 0, 1) : ACTION_GO_BACK,
  (1, 0, 0, 1) : ACTION_GO_BACK_LEFT,
  (0, 1, 0, 1) : ACTION_GO_BACK_RIGHT,
  }

class HumanController(soya.tofu4soya.LocalController):
  def __init__(self, mobile):
    soya.tofu4soya.LocalController.__init__(self, mobile)
    
    self.left_key_down = self.right_key_down = self.up_key_down = self.down_key_down = 0
    self.look_key_down = 0
    self.strike_key_down = 0
    self.current_deplacement = ACTION_WAIT
    
  def big_round(self): pass
  
  def begin_round(self):
    for event in soya.process_event():
      event_type = event[0]
      if (event_type == sdlconst.KEYDOWN) or (event_type == sdlconst.KEYUP) or (event_type == sdlconst.JOYBUTTONDOWN) or (event_type == sdlconst.JOYBUTTONUP) or (event_type == sdlconst.JOYAXISMOTION):
        if event_type == sdlconst.JOYAXISMOTION:
          
          if   event[1] == 0:
            if   event[2] < 0: event_type = sdlconst.KEYDOWN; control = CONTROL_LEFT
            elif event[2] > 0: event_type = sdlconst.KEYDOWN; control = CONTROL_RIGHT
            else:
              event_type = sdlconst.KEYUP
              if self.left_key_down: control = CONTROL_LEFT
              else:                  control = CONTROL_RIGHT
          elif event[1] == 1:
            if   event[2] < 0: event_type = sdlconst.KEYDOWN; control = CONTROL_UP
            elif event[2] > 0: event_type = sdlconst.KEYDOWN; control = CONTROL_DOWN
            else:
              event_type = sdlconst.KEYUP
              if self.up_key_down  : control = CONTROL_UP
              else:                  control = CONTROL_DOWN
        else:
          control = CONTROLS.get_control(event)
          
        if control != CONTROL_UNKNOWN:
          
          if (event_type == sdlconst.KEYDOWN) or (event_type == sdlconst.JOYBUTTONDOWN):
            for active_widget in tofu.GAME_INTERFACE.active_widgets:
              if active_widget.key_down(control): break
            else:
              if   control == CONTROL_QUIT:
                import balazar.game_interface
                d = balazar.game_interface.QuitMenu(self.mobile).activate(1)
                #tofu.GAME_INTERFACE.end_game() # Quit the game
              elif control == CONTROL_JUMP:   self.mobile.doer.do_action(Action(ACTION_JUMP))
              elif control == CONTROL_LEFT:   self.left_key_down  = 1
              elif control == CONTROL_RIGHT:  self.right_key_down = 1
              elif control == CONTROL_UP:     self.up_key_down    = 1
              elif control == CONTROL_DOWN:   self.down_key_down  = 1
              elif control == CONTROL_STRIKE:
                self.strike_key_down = 1
                if self.mobile.weapon: self.mobile.doer.do_action(Action(ACTION_GUARD))
              elif control == CONTROL_MENU:
                import balazar.game_interface, balazar.inventory
                if not tofu.GAME_INTERFACE.active_widgets:
                  balazar.game_interface.Bubble(soya.root_widget, self.mobile, balazar.inventory.Inventory(self.mobile)).set_focus(1)
              elif control == CONTROL_SUICIDE: self.mobile.doer.do_action(Action(ACTION_KILLED))
              elif control == CONTROL_LOOK_AT_LEFT:  self.look_key_down = 1
              elif control == CONTROL_LOOK_AT_RIGHT: self.look_key_down = 1
              elif control == CONTROL_PAUSE:
                # Hack !! remove Idler.render, because it would render an empty scene.
                soya.IDLER.render = lambda : None
                tofu.GAME_INTERFACE.end_game("paused") # Quit the game, and relaunch it after the pause
                
              elif control == CONTROL_SCREENSHOT:
                import tempfile
                screenshot_file = tempfile.mktemp(".jpeg")
                soya.screenshot(screenshot_file)
                print "* Balazar * Screenshot saved as %s" % screenshot_file
                
          else: # Up
            if   control == CONTROL_JUMP:   self.mobile.doer.do_action(Action(ACTION_STOP_JUMPING))
            elif control == CONTROL_LEFT:   self.left_key_down  = 0
            elif control == CONTROL_RIGHT:  self.right_key_down = 0
            elif control == CONTROL_UP:     self.up_key_down    = 0
            elif control == CONTROL_DOWN:   self.down_key_down  = 0
            elif control == CONTROL_STRIKE:
              self.strike_key_down = 0
              self.auto_strike()
            elif control == CONTROL_LOOK_AT_LEFT:
              if (self.look_key_down == 1) and not tofu.GAME_INTERFACE.traveling.is_in_fight_mode():
                tofu.GAME_INTERFACE.traveling.lateral_angle -= math.pi / 2.0
                if tofu.GAME_INTERFACE.traveling.lateral_angle < -math.pi: tofu.GAME_INTERFACE.traveling.lateral_angle += 2 * math.pi
              self.look_key_down = 0
            elif control == CONTROL_LOOK_AT_RIGHT:
              if (self.look_key_down == 1) and not tofu.GAME_INTERFACE.traveling.is_in_fight_mode():
                tofu.GAME_INTERFACE.traveling.lateral_angle += math.pi / 2.0
                if tofu.GAME_INTERFACE.traveling.lateral_angle >  math.pi: tofu.GAME_INTERFACE.traveling.lateral_angle -= 2 * math.pi
              self.look_key_down = 0
        
              
    if self.look_key_down and not tofu.GAME_INTERFACE.traveling.is_in_fight_mode():
      from balazar.game_interface import CameraTraveling
      
      traveling = tofu.GAME_INTERFACE.traveling
      
      if self.left_key_down:
        self.look_key_down = 2
        traveling.lateral_angle -= math.pi / 2.0
        if traveling.lateral_angle < -math.pi: traveling.lateral_angle += 2 * math.pi
        self.left_key_down = 0
        
      elif self.right_key_down:
        self.look_key_down = 2
        traveling.lateral_angle += math.pi / 2.0
        if traveling.lateral_angle >  math.pi: traveling.lateral_angle -= 2 * math.pi
        self.right_key_down = 0
        
      elif self.up_key_down:
        self.look_key_down = 2
        if isinstance(tofu.GAME_INTERFACE.camera.travelings[-1], CameraTraveling): tofu.GAME_INTERFACE.camera.travelings[-1].look_toward(1)
        else:
          if traveling.distance > 4.0:
            traveling.distance -= 0.5
            traveling.top_view -= 0.0283
          elif traveling.offset_y2 < 4.0: traveling.offset_y2 += 0.1
          
      elif self.down_key_down:
        self.look_key_down = 2
        if isinstance(tofu.GAME_INTERFACE.camera.travelings[-1], CameraTraveling): tofu.GAME_INTERFACE.camera.travelings[-1].look_toward(-1)
        else:
          if   traveling.offset_y2 > 1.0: traveling.offset_y2 -= 0.1
          elif traveling.distance < 18.0:
            traveling.distance += 0.5
            traveling.top_view += 0.0283
            
      self.mobile.doer.do_action(None)
    else:
      if self.strike_key_down:
        action = GUARD_CONTROL_2_ACTION .get((self.left_key_down, self.right_key_down), ACTION_WAIT)
      else:
        action = NORMAL_CONTROL_2_ACTION.get((self.left_key_down, self.right_key_down, self.up_key_down, self.down_key_down), ACTION_WAIT)
        
      if action == self.current_deplacement:
        if self.strike_key_down and (not self.mobile.current_action) and self.mobile.weapon: # The strike key has been pressed while another action was doing
          self.mobile.doer.do_action(Action(ACTION_GUARD))
        else:
          self.mobile.doer.do_action(None)
      else:
        self.current_deplacement = action
        self.mobile.doer.do_action(Action(action))
    return 1
  
  def find_fight_target(self):
    target = None
    best_dist = 10000.0
    for s in self.mobile.level:
      if isinstance(s, base.Strikeable) or (isinstance(s, Character) and (not s is self.mobile) and (s.life > 0.0) and self.mobile.is_enemy(s)):
        dist = self.mobile.distance_to(s)
        if   isinstance(s, Character): dist = dist / 2.0 # Characters are targetted prioritarily than mushrooms !
        elif dist > 15.0: continue # Too far away
        if dist < best_dist:
          best_dist = dist
          target    = s
          
    return target
  
  def auto_strike(self):
    if not self.mobile.current_animation.startswith("combat"):
      target = self.find_fight_target()
      
      if target and isinstance(target, Character):
        tofu.GAME_INTERFACE.traveling.set_enemy(target)
        
      if self.left_key_down or self.right_key_down or self.up_key_down or self.down_key_down:
        if self.left_key_down : self.mobile.doer.do_action(FightAction(ACTION_FIGHT_LEFT    , target)); return
        if self.right_key_down: self.mobile.doer.do_action(FightAction(ACTION_FIGHT_RIGHT   , target)); return
        if self.up_key_down   : self.mobile.doer.do_action(FightAction(ACTION_FIGHT_CHARGE  , target)); return
        if self.down_key_down : self.mobile.doer.do_action(FightAction(ACTION_FIGHT_SAGITTAL, target)); return
        
      if target:
        _P.clone(target)
        _P.convert_to(self.mobile)
        c = _P.x / abs(_P.z)
        if self.mobile.weapon: range = self.mobile.range + self.mobile.weapon.range
        else:                  range = self.mobile.range
        if (_P.z < -1.0 - range) and (-0.6 < c < 0.6):
          self.mobile.doer.do_action(FightAction(ACTION_FIGHT_CHARGE, target))
        elif c < -0.6: self.mobile.doer.do_action(FightAction(ACTION_FIGHT_LEFT , target))
        elif c >  0.6: self.mobile.doer.do_action(FightAction(ACTION_FIGHT_RIGHT, target))
        elif not -1.0 < _P.y < 1.0: self.mobile.doer.do_action(FightAction(ACTION_FIGHT_SAGITTAL, target))
        elif c < 0.0:  self.mobile.doer.do_action(FightAction(ACTION_FIGHT_LEFT , target))
        else:          self.mobile.doer.do_action(FightAction(ACTION_FIGHT_RIGHT, target))
      else:
        self.mobile.doer.do_action(FightAction(ACTION_FIGHT_CHARGE, target))
      











def Wait(self, duration):
  yield Action(ACTION_WAIT)
  for i in range(duration - 1): yield None

def Wander(self, duration):
  for i in range(4):
    p = soya.Point(self, (random.random() - 0.5) * 10.0, 2.0, (random.random() - 0.75) * 12.0)
    p.convert_to(self.level)
    if self.level.raypick_b(p, self.down, 5.0):
      yield GotoXZ(self, p, 2.0)
      break
  else:
    yield Wait(self, 100)
    
def LookAt(self, target, limit = 0.3):
  action = -1
  current_action = ACTION_WAIT
  while 1:
    tx, ty, tz = self.transform_point(target.x, target.y, target.z, target.parent)
    
    if tz < 0.0:
      txz = tx / tz
      
      if   txz < -limit: action = ACTION_TURN_RIGHT
      elif txz >  limit: action = ACTION_TURN_LEFT
      else: return
    else:
      if tx < 0.0: action = ACTION_TURN_LEFT
      else:        action = ACTION_TURN_RIGHT
      
    if action == current_action: yield None
    else:                        yield Action(action)
    
def Goto(self, target, limit = 1.0):
  action = -1
  current_action = ACTION_WAIT
  while self.distance_to(target) > limit:
    tx, ty, tz = self.transform_point(target.x, target.y, target.z, target.parent)
    
    if tz < 0.0:
      txz = tx / tz
      
      if   txz < -1.0: action = ACTION_TURN_RIGHT
      elif txz >  1.0: action = ACTION_TURN_LEFT
      elif self.on_ground and (ty > 3.0) and (tz > -10.0): yield Action(ACTION_JUMP)
      elif txz < -0.3: action = ACTION_ADVANCE_RIGHT
      elif txz >  0.3: action = ACTION_ADVANCE_LEFT
      else:            action = ACTION_ADVANCE
      
    else:
      if tx < 0.0: action = ACTION_TURN_LEFT
      else:        action = ACTION_TURN_RIGHT
      
    if action == current_action: yield None
    else:                        yield Action(action)
    
def GotoXZ(self, target, limit = 1.0):
  action = -1
  current_action = ACTION_WAIT
  while self.distance_to(target) > limit:
    tx, ty, tz = self.transform_point(target.x, self.y, target.z, target.parent)
    
    if tz < 0.0:
      txz = tx / tz
      
      if   txz < -1.0: action = ACTION_TURN_RIGHT
      elif txz >  1.0: action = ACTION_TURN_LEFT
      elif txz < -0.3: action = ACTION_ADVANCE_RIGHT
      elif txz >  0.3: action = ACTION_ADVANCE_LEFT
      else:            action = ACTION_ADVANCE
      
    else:
      if tx < 0.0: action = ACTION_TURN_LEFT
      else:        action = ACTION_TURN_RIGHT
      
    if action == current_action: yield None
    else:                        yield Action(action)
    
def Gotoward(self, target, duration, limit = 1.0):
  action = -1
  current_action = ACTION_WAIT
  while (duration > 0) and (self.distance_to(target) > limit):
    duration -= 1
    tx, ty, tz = self.transform_point(target.x, target.y, target.z, target.parent)
    
    if tz < 0.0:
      txz = tx / tz
      
      if   txz < -1.0: action = ACTION_TURN_RIGHT
      elif txz >  1.0: action = ACTION_TURN_LEFT
      elif self.on_ground and (ty > 3.0) and (tz > -10.0): yield Action(ACTION_JUMP)
      elif txz < -0.3: action = ACTION_ADVANCE_RIGHT
      elif txz >  0.3: action = ACTION_ADVANCE_LEFT
      else:            action = ACTION_ADVANCE
      
    else:
      if tx < 0.0: action = ACTION_TURN_LEFT
      else:        action = ACTION_TURN_RIGHT
      
    if action == current_action: yield None
    else:                        yield Action(action)

def Fly(self, target, duration = 50):
  action = -1
  current_action = ACTION_WAIT
  for i in range(duration):
    tx, ty, tz = self.transform_point(target.x, target.y, target.z, target.parent)
    
    if tz > 0.0:
      txz = tx / tz
      
      if   txz < -1.0: action = ACTION_TURN_RIGHT
      elif txz >  1.0: action = ACTION_TURN_LEFT
      elif txz < -0.3: action = ACTION_ADVANCE_RIGHT
      elif txz >  0.3: action = ACTION_ADVANCE_LEFT
      else:            action = ACTION_ADVANCE
      
    else:
      if tx > 0.0: action = ACTION_ADVANCE_LEFT
      else:        action = ACTION_TURN_RIGHT
      
    if action == current_action: yield None
    else:                        yield Action(action)

def GrabItem(self, item):
  yield Goto(self, item)
  if not item.owner: # Else someone already grab it !
    self.doer.do_action(ItemAction(ACTION_GRAB_ITEM, item))








