# A basic 11 button dance pad

from constants import *
import pygame, os
import cPickle as pickle
from pygame.locals import *
import colors

(PASS, QUIT, UP, UPLEFT, LEFT, DOWNLEFT, DOWN, DOWNRIGHT,
 RIGHT, UPRIGHT, CENTER, START, SELECT, SCREENSHOT) = range(14)

NAMES = ["", "quit", "up", "up-left", "left", "down-left", "down",
         "down-right", "right", "up-right", "center", "start", "select"]

KEYS = {
  K_ESCAPE: QUIT,
  K_PRINT: SCREENSHOT,
  K_SYSREQ: SCREENSHOT,
  }

KEY1 = {
  K_KP7: UPLEFT,
  K_KP8: UP,
  K_KP9: UPRIGHT,
  K_KP4: LEFT,
  K_KP5: CENTER,
  K_KP6: RIGHT,
  K_KP1: DOWNLEFT,
  K_KP2: DOWN,
  K_KP3: DOWNRIGHT,
  K_KP_ENTER: START,
  K_KP0: SELECT,
}

KEY2 = {
  K_u: UPLEFT,
  K_i: UP,
  K_o: UPRIGHT,
  K_j: LEFT,
  K_k: CENTER,
  K_l: RIGHT,
  K_m: DOWNLEFT,
  K_COMMA: DOWN,
  K_PERIOD: DOWNRIGHT,
  K_7: SELECT,
  K_9: START,
  }

# 16 buttons, 4 axis
# The EMSUSB2 is one adapter, two joysticks - +16 to get p2, uses this
# too. Apparently, there are two possible start buttons, 9 and 11?

# L1: 4, L2: 6, R1: 7, R2: 5
# Left: 15, Up: 12, Right: 13, Down: 14
# Square: 3, Tri: 0, Circle: 1, X: 2
# Select: 8, start: 9
# Left analog: 10, Right analog: 11

A4B16 = { 0: DOWNLEFT, 1: UPRIGHT, 2: UPLEFT, 3: DOWNRIGHT, 5: CENTER,
          8: SELECT, 9: START, 11: START, 12: UP, 13: RIGHT, 14: DOWN,
          15: LEFT }

# Buy n Shop parallel adapter
A6B12 = { 1: UPRIGHT, 3: UPLEFT, 8: SELECT, 4: LEFT, 5: RIGHT,
          6: DOWN, 7: UP, 9: START }

# Gravis Gamepad Pro USB - Uses axes for directions.
# 0: Square, 1: X, 2: O, 3: Triangle
# 4: L1, 5: R1, 6: L2, 7: R2
# 8: Select, 9: Start
A2B10 = { 0: LEFT, 1: DOWN, 2: RIGHT, 3: UP, 5: UPRIGHT, 7: DOWNRIGHT,
          8: SELECT, 9: START }
  
# This is some sort of natively USB mat
A2B8 = { 4: UP, 6: DOWN,  5: LEFT, 7: RIGHT, 2: START,
         3: DOWNLEFT, 0: UPRIGHT, 1: DOWNRIGHT }

# LevelSix USB pad? I'm not at all sure this is right.
A3B10 = { 0: LEFT, 1: DOWN, 2: RIGHT, 3: UP, 4: UPLEFT, 5: UPRIGHT,
          6: DOWNLEFT, 7: DOWNRIGHT, 8: SELECT, 9: START }
  
# Xbox gamepads, Linux driver
# With joydev module: 8 axes, 10 buttons
# With evdev module: 6 axes, 1 hat, 10 buttons
# B: 1, A: 0, X: 3, Y: 4, White: 5, Black: 2
# Back: 9, Start: 6
# Left analog stick: 7, Right analog stick: 8
A8B10 = { 4: UP, 0: DOWN, 3: LEFT, 1: RIGHT, 6: START, 9: QUIT }

# XBox gamepads, Xbox Linux driver (2.4.25 and 2.6.4 and earlier)
# With joydev module: 14 axes, 10 buttons
# With evdev module: 6 axes, 4 hats, 10 buttons
# B: 1, A: 0, X: 3, Y: 4, White: 5, Black: 2
# Back: 9, Start: 6
# Left analog stick: 7, Right analog stick: 8
A14B10 = { 4: UP, 0: DOWN, 3: LEFT, 1: RIGHT, 6: START, 9: QUIT }

# XBox gamepads, Xbox Linux driver
# With joydev module: 14 axes, 14 buttons
# With evdev module: 6 axes, 4 hats, 14 buttons
# Left: 12, Up: 9, Right: 10, Down: 11
# B: 1, A: 0, X: 3, Y: 4, White: 5, Black: 2
# Back: 13, Start: 6
# Left analog stick: 7, Right analog stick: 8
A14B14 = { 13: QUIT, 9: UP, 1: UPLEFT, 12: LEFT, 4: DOWNLEFT, 11: DOWN,
           3: DOWNRIGHT, 10: RIGHT, 0: UPRIGHT, 6: START }

MATS = { (6, 12): A6B12, (14, 10): A14B10, (2, 10): A2B10, (2, 8): A2B8,
         (8, 10): A8B10, (14, 14): A14B14 }

class Pad(object):

  (PASS, QUIT, UP, UPLEFT, LEFT, DOWNLEFT, DOWN, DOWNRIGHT,
   RIGHT, UPRIGHT, CENTER, START, SELECT, SCREENSHOT) = range(14)

  def __init__(self, handler = pygame.event):
    self.handler = handler
    self.handler.set_blocked(range(NUMEVENTS))
    self.handler.set_allowed((KEYUP, KEYDOWN, JOYBUTTONUP, JOYBUTTONDOWN))

    self.states = {}
    self.events = {}

    mat = mat2 = emsusb2 = None

    try: totaljoy = pygame.joystick.get_count()
    except: totaljoy = 0

    print totaljoy, "joystick(s) found."

    # Initialize all the joysticks, print diagnostics.
    for i in range(totaljoy):
      m = pygame.joystick.Joystick(i)
      m.init()
      args = (i, m.get_numaxes(), m.get_numbuttons())
      # One hat is two axes.
      args = (i, m.get_numaxes() + 2 * m.get_numhats(), m.get_numbuttons())
      print "Joystick %d initialized: %d axes, %d buttons." % args

      if args[2] == 32: emsusb2 = i
      elif mat == None and (args[1], args[2]) in MATS: mat = i
      elif mat2 == None and (args[1], args[2]) in MATS: mat2 = i

    self.merge_events(0, -1, KEY1)
    self.merge_events(1, -1, KEY2)

    loaded_input = False
    if os.path.exists(os.path.join(rc_path, "input.cfg")):
      try:
        fn = os.path.join(rc_path, "input.cfg")
        self.events = pickle.load(file(fn, "r"))
        for ev in self.events.values(): self.states[ev] = False
        loaded_input = True
      except:
        print "W: Unable to load input configuration file."
        loaded_input = False

    if loaded_input:
      print "Loaded input configuration."
    elif emsusb2 != None:
      self.merge_events(0, emsusb2, A4B16) 
      self.merge_events(1, emsusb2, dict([(k + 16, v) for (k, v) in A4B16.items()]))
      print "EMSUSB2 found. Using preset EMSUSB2 config."
    elif mat != None:
      joy = pygame.joystick.Joystick(mat)
      axes, but = joy.get_numaxes() + 2 * joy.get_numhats(), joy.get_numbuttons()
      print "Initializing player 1 using js%d." % mat
      self.merge_events(0, mat, MATS[(axes, but)])

      if mat2:
        joy = pygame.joystick.Joystick(mat2)
        axes, but = joy.get_numaxes() + 2 * joy.get_numhats(), joy.get_numbuttons()
        print "Initializing player 2 using js%d." % mat2
        self.merge_events(1, mat2, MATS[(axes, but)])
    elif totaljoy > 0:
      print "No known joysticks found! If you want to use yours,"
      print "you'll have to map its button manually once to use it."

    self.merge_events(-1, -1, KEYS)

  def reinit_pads(self):
    pygame.joystick.init()
    try: totaljoy = pygame.joystick.get_count()
    except: totaljoy = 0
    print totaljoy, "joystick(s) found."
    for i in range(totaljoy): pygame.joystick.Joystick(i).init()

  def add_event(self, device, key, pid, event):
    self.events[(device, key)] = (pid, event)
    self.states[(pid, event)] = False

  def merge_events(self, pid, device, events):
    for key, event in events.items():
      self.add_event(device, key, pid, event)

  def device_key_for(self, keyboard, pid, event):
    for (device, key), (p, e) in self.events.items():
      if p == pid and e == event:
        if keyboard and device == -1: return pygame.key.name(key)
        elif not keyboard and device != -1: return "%d:%d" % (device, key)
    return "---"

  def delete_event(self, pid, keyb, event):
    for (d, k), (p, e) in self.events.items():
      if (p == pid and e == event and
          ((d == -1 and keyb) or (d != -1 and not keyb))):
        del(self.events[(d, k)])
        self.states[(d, k)] = False

  def delete_events(self, pid):
    for k, v in self.events.items():
      if v[0] == pid: del(self.events[k])
      self.states[v] == False

  # Poll the event handler and normalize the result. If we don't know
  # about the event but the device is the keyboard, return (-2, key).
  # Otherwise, return pass.
  def poll(self):
    ev = self.handler.poll()
    t = -1
    v = 0
    if ev.type == JOYBUTTONDOWN or ev.type == JOYBUTTONUP:
      t, v = ev.joy, ev.button
    elif ev.type == KEYDOWN or ev.type == KEYUP:
      t, v = -1, ev.key
    else:
      return (-1, PASS)

    # Pass in all keyboard keys pressed, so the ui handler can get them.
    # Also, if arrow keys are pressed return their direction even if they're
    # not mapped.
    if ((ev.type == KEYDOWN or ev.type == KEYUP) and
        (ev.key == K_LEFT or ev.key == K_RIGHT or ev.key == K_UP or
         ev.key == K_DOWN)):
      default = (0, {K_LEFT: LEFT, K_RIGHT: RIGHT,
                     K_UP: UP, K_DOWN: DOWN}[ev.key])
    elif (ev.type == KEYUP or ev.type == KEYDOWN):
      default = (-2, ev.key * 100)
    else: default = (-1, PASS)

    ret = self.events.get((t, v), default)

    if ev.type == JOYBUTTONUP or ev.type == KEYUP:
      if ret[0] != -2: self.states[ret] = False
      ret = (ret[0], -ret[1])
    elif ev.type == JOYBUTTONDOWN or ev.type == KEYDOWN:
      if ret[0] != -2: self.states[ret] = True

    return ret

  def wait(self, delay = 20):
    ev = (-1, PASS)
    while ev[1] == PASS:
      ev = self.poll()
      pygame.time.wait(delay)
    return ev

  def empty(self):
    ev = (0, QUIT)
    while ev[1] != PASS: ev = self.poll()

  def write(self, fn):
    pickle.dump(self.events, file(fn, "w"), 2)

  def set_repeat(*args): pass

pad = Pad()

class PadConfig(object):

  bg = pygame.image.load(os.path.join(image_path, "bg.png"))

  directions = range(2, 13)

  def __init__(self, screen):
    self.screen = screen
    pad.reinit_pads()
    clock = pygame.time.Clock()
    self.loc = [0, 0]
    self.width = [4, 11]

    ev = pygame.event.poll()
    while ev.type != KEYDOWN or ev.key != K_ESCAPE:
      self.render()
      ev = pygame.event.poll()

      if ev.type == KEYDOWN:
        if ev.key in [K_LEFT, K_KP4]: self.loc[0] -= 1
        elif ev.key in [K_RIGHT, K_KP6]: self.loc[0] += 1
        elif ev.key in [K_UP, K_KP8]: self.loc[1] -= 1
        elif ev.key in [K_DOWN, K_KP2]: self.loc[1] += 1
        elif ev.key in [K_RETURN, K_KP_ENTER]: self.map_key()

      self.loc[0] %= 4
      self.loc[1] %= 11

      clock.tick(30)

  def map_key(self):
    dir = self.loc[1] + 2
    if self.loc[0] % 2 == 0: keyb = True
    else: keyb = False
    if self.loc[0] > 1: pid = 1
    else: pid = 0

    if keyb:
      wanted_type = KEYDOWN
      text = "Press a key for %s (escape to cancel)" % NAMES[dir]
    else:
      wanted_type = JOYBUTTONDOWN
      text = "Press a button for %s (escape to cancel)" % NAMES[dir]

    img = FONTS[32].render(text, True, colors.BLACK)
    r = img.get_rect()
    r.midbottom = [320, 460]
    self.screen.blit(img, r)
    pygame.display.update()

    ev = pygame.event.wait()
    while (ev.type != wanted_type and
           (ev.type != KEYDOWN or ev.key != K_ESCAPE)):
      ev = pygame.event.wait()
    if ev.type != KEYDOWN or ev.key != K_ESCAPE:
      if keyb: dev, but = -1, ev.key
      else: dev, but = ev.joy, ev.button
      pad.delete_event(pid, keyb, dir)
      pad.add_event(dev, but, pid, dir)

  def render(self):
    offset = 640 / 5
    cent = offset / 2

    self.screen.blit(PadConfig.bg, [0, 0])

    text = ["Player 1", "Player 2"]
    text2 = ["Keyboard", "Joystick"]

    for t in text:
      img = FONTS[32].render(t, True, colors.BLACK)
      r = img.get_rect()
      r.midtop = [160 + 320 * text.index(t), 15]
      self.screen.blit(img, r)

      for t2 in text2:
        img = FONTS[28].render(t2, True, colors.BLACK)
        r = img.get_rect()
        r.midtop = [cent + offset * text2.index(t2) +
                    (offset * 3) * text.index(t), 50]
        self.screen.blit(img, r)

    
    for dir in PadConfig.directions:
      p1_key = pad.device_key_for(True, 0, dir)
      p1_joy = pad.device_key_for(False, 0, dir)
      p2_key = pad.device_key_for(True, 1, dir)
      p2_joy = pad.device_key_for(False, 1, dir)

      order = [p1_key, p1_joy, NAMES[dir], p2_key, p2_joy]
      for i in range(len(order)):
        if (dir == self.loc[1] + 2 and
            ((i < 2 and i == self.loc[0]) or
             (i > 2 and i == self.loc[0] + 1))):
          img = FONTS[26].render(order[i], True, colors.WHITE)
        else: img = FONTS[26].render(order[i], True, colors.BLACK)
        r = img.get_rect()
        r.center = [cent + offset * i, 60 + 26 * dir]
        self.screen.blit(img, r)

    pygame.display.update()
