#####################################################################
# -*- coding: iso-8859-1 -*-                                        #
#                                                                   #
# Frets on Fire                                                     #
# Copyright (C) 2006 Sami Kyöstilä                                  #
#                                                                   #
# 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., 51 Franklin Street, Fifth Floor, Boston,        #
# MA  02110-1301, USA.                                              #
#####################################################################

"""A bunch of dialog functions for interacting with the user."""

import pygame
from OpenGL.GL import *
from OpenGL.GLU import *
import math
import os
import fnmatch

from View import Layer, BackgroundLayer
from Input import KeyListener
from Camera import Camera
from Mesh import Mesh
from Menu import Menu
from Language import _
from Texture import Texture
import Theme
import Log
import Song
import Data
import Player
import Guitar

def wrapText(font, pos, text, rightMargin = 0.9, scale = 0.002, visibility = 0.0):
  """
  Wrap a piece of text inside given margins.
  
  @param pos:         (x, y) tuple, x defines the left margin
  @param text:        Text to wrap
  @param rightMargin: Right margin
  @param scale:       Text scale
  @param visibility:  Visibility factor [0..1], 0 is fully visible
  """
  x, y = pos
  space = font.getStringSize(" ", scale = scale)[0]

  for n, word in enumerate(text.split(" ")):
    w, h = font.getStringSize(word, scale = scale)
    if x + w > rightMargin or word == "\n":
      x = pos[0]
      y += h
    if word == "\n":
      continue
    glPushMatrix()
    glRotate(visibility * (n + 1) * -45, 0, 0, 1)
    font.render(word, (x, y + visibility * n), scale = scale)
    glPopMatrix()
    x += w + space
  return (x - space, y)

def fadeScreen(v):
  """
  Fade the screen to a dark color to make whatever is on top easier to read.
  
  @param v: Visibility factor [0..1], 0 is fully visible
  """
  glEnable(GL_BLEND)
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  glEnable(GL_COLOR_MATERIAL)

  glBegin(GL_TRIANGLE_STRIP)
  glColor4f(0, 0, 0, .3 - v * .3)
  glVertex2f(0, 0)
  glColor4f(0, 0, 0, .3 - v * .3)
  glVertex2f(1, 0)
  glColor4f(0, 0, 0, .9 - v * .9)
  glVertex2f(0, 1)
  glColor4f(0, 0, 0, .9 - v * .9)
  glVertex2f(1, 1)
  glEnd()
  

class GetText(Layer, KeyListener):
  """Text input layer."""
  def __init__(self, engine, prompt = "", text = ""):
    self.text = text
    self.prompt = prompt
    self.engine = engine
    self.time = 0
    self.accepted = False
    
  def shown(self):
    self.engine.input.addKeyListener(self, priority = True)
    self.engine.input.enableKeyRepeat()
    
  def hidden(self):
    self.engine.input.removeKeyListener(self)
    self.engine.input.disableKeyRepeat()
    
  def keyPressed(self, key, unicode):
    self.time = 0
    c = self.engine.input.controls.getMapping(key)
    if unicode and ord(unicode) > 31 and not self.accepted:
      self.text += unicode
    elif (c in [Player.KEY1] or key == pygame.K_RETURN) and not self.accepted:
      self.engine.view.popLayer(self)
      self.accepted = True
    elif c in [Player.CANCEL, Player.KEY2] and not self.accepted:
      self.text = None
      self.engine.view.popLayer(self)
      self.accepted = True
    elif key == pygame.K_BACKSPACE and not self.accepted:
      self.text = self.text[:-1]
    return True
    
  def run(self, ticks):
    self.time += ticks / 50.0
  
  def render(self, visibility, topMost):
    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font
    
    try:
      v = (1 - visibility) ** 2
      
      fadeScreen(v)
      Theme.setBaseColor(1 - v)

      if (self.time % 10) < 5 and visibility > .9:
        cursor = "|"
      else:
        cursor = ""

      pos = wrapText(font, (.1, .33 - v), self.prompt)

      Theme.setSelectedColor(1 - v)
      
      if self.text is not None:
        pos = wrapText(font, (.1, (pos[1] + v) + .08 + v / 4), self.text)
        font.render(cursor, pos)
      
    finally:
      self.engine.view.resetProjection()

class GetKey(Layer, KeyListener):
  """Key choosing layer."""
  def __init__(self, engine, prompt = "", key = None):
    self.key = key
    self.prompt = prompt
    self.engine = engine
    self.time = 0
    self.accepted = False
    
  def shown(self):
    self.engine.input.addKeyListener(self, priority = True)
    
  def hidden(self):
    self.engine.input.removeKeyListener(self)
    
  def keyPressed(self, key, unicode):
    c = self.engine.input.controls.getMapping(key)
    if c in [Player.CANCEL, Player.KEY2] and not self.accepted:
      self.key = None
      self.engine.view.popLayer(self)
      self.accepted = True
    elif not self.accepted:
      self.key = key
      self.engine.view.popLayer(self)
      self.accepted = True
    return True
    
  def run(self, ticks):
    self.time += ticks / 50.0
  
  def render(self, visibility, topMost):
    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font
    
    try:
      v = (1 - visibility) ** 2
      
      fadeScreen(v)
      Theme.setBaseColor(1 - v)

      pos = wrapText(font, (.1, .33 - v), self.prompt)

      Theme.setSelectedColor(1 - v)

      if self.key is not None:
        text = pygame.key.name(self.key).capitalize()
        pos = wrapText(font, (.1, (pos[1] + v) + .08 + v / 4), text)
      
    finally:
      self.engine.view.resetProjection()

class LoadingScreen(Layer, KeyListener):
  """Loading screen layer."""
  def __init__(self, engine, condition, text, allowCancel = False):
    self.engine      = engine
    self.text        = text
    self.condition   = condition
    self.ready       = False
    self.allowCancel = allowCancel
    self.time        = 0.0
    
  def shown(self):
    self.engine.input.addKeyListener(self, priority = True)

  def keyPressed(self, key, unicode):
    c = self.engine.input.controls.getMapping(key)
    if self.allowCancel and c == Player.CANCEL:
      self.engine.view.popLayer(self)
    return True
    
  def hidden(self):
    self.engine.input.removeKeyListener(self)

  def run(self, ticks):
    self.time += ticks / 50.0
    if not self.ready and self.condition():
      self.engine.view.popLayer(self)
      self.ready = True
  
  def render(self, visibility, topMost):
    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font

    if not font:
      return
    
    try:
      v = (1 - visibility) ** 2
      fadeScreen(v)

      Theme.setBaseColor(1 - v)
      w, h = font.getStringSize(self.text)
      x = .5 - w / 2
      y = .5 - h / 2 + v
      
      font.render(self.text, (x, y))
      
    finally:
      self.engine.view.resetProjection()

class MessageScreen(Layer, KeyListener):
  """Message screen layer."""
  def __init__(self, engine, text, prompt = _("<OK>")):
    self.engine = engine
    self.text = text
    self.time = 0.0
    self.prompt = prompt

  def shown(self):
    self.engine.input.addKeyListener(self, priority = True)

  def keyPressed(self, key, unicode):
    c = self.engine.input.controls.getMapping(key)
    if c in [Player.KEY1, Player.KEY2, Player.CANCEL] or key == pygame.K_RETURN:
      self.engine.view.popLayer(self)
    return True
    
  def hidden(self):
    self.engine.input.removeKeyListener(self)

  def run(self, ticks):
    self.time += ticks / 50.0
  
  def render(self, visibility, topMost):
    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font

    if not font:
      return
    
    try:
      v = (1 - visibility) ** 2
      fadeScreen(v)

      x = .1
      y = .3 + v * 2
      Theme.setBaseColor(1 - v)
      pos = wrapText(font, (x, y), self.text, visibility = v)

      w, h = font.getStringSize(self.prompt, scale = 0.001)
      x = .5 - w / 2
      y = pos[1] + 3 * h + v * 2
      Theme.setSelectedColor(1 - v)
      font.render(self.prompt, (x, y), scale = 0.001)
      
    finally:
      self.engine.view.resetProjection()
      
class SongChooser(Layer, KeyListener):
  """Song choosing layer."""
  def __init__(self, engine, prompt = "", selectedSong = None):
    self.prompt         = prompt
    self.engine         = engine
    self.time           = 0
    self.accepted       = False
    self.selectedIndex  = 0
    self.camera         = Camera()
    self.cassetteHeight = .8
    self.cassetteWidth  = 4.0
    self.cassetteAngles = None
    self.cassetteLabels = None
    self.loaded         = False
    self.selectedOffset = 0.0
    self.cameraOffset   = 0.0
    self.selectedSong   = None
    self.song           = None
    self.songCountdown  = 1024
    self.songLoader     = None
    self.initialSong    = selectedSong
    
    self.engine.resource.load(self, "songs",        lambda: Song.getAvailableSongs(self.engine), onLoad = self.songListLoaded)
    self.engine.resource.load(self, "cassette",     lambda: Mesh(self.engine.resource.fileName("cassette.dae")), synch = True)
    self.engine.resource.load(self, "label",        lambda: Mesh(self.engine.resource.fileName("label.dae")), synch = True)
    showLoadingScreen(self.engine, lambda: self.loaded, text = _("Browsing Collection..."))
    
    self.engine.loadSvgDrawing(self, "background", "cassette.svg")
    
  def songListLoaded(self, songs):
    if self.initialSong is not None:
      for i, song in enumerate(songs):
        if self.initialSong == song[0]:
          self.selectedIndex =  i
          break
    self.cassetteAngles = [0.0] * len(self.songs)
    self.cassetteLabels = [None] * len(self.songs) 
    self.loaded = True
    self.updateSelection()
    
  def shown(self):
    self.engine.input.addKeyListener(self, priority = True)
    self.engine.input.enableKeyRepeat()
    
  def hidden(self):
    if self.songLoader:
      self.songLoader.cancel()
    if self.song:
      self.song.fadeout(1000)
      self.song = None
    self.engine.input.removeKeyListener(self)
    self.engine.input.disableKeyRepeat()
    
  def getSelectedSong(self):
    return self.selectedSong

  def updateSelection(self):
    self.selectedSong = self.songs[self.selectedIndex][0]
    self.songCountdown = 1024
    
    # Load the cassette label if it isn't yet loaded
    if self.cassetteLabels[self.selectedIndex] is None:
      label = self.engine.resource.fileName("songs", self.selectedSong, "label.png")
      if os.path.exists(label):
        self.cassetteLabels[self.selectedIndex] = Texture(label)
 
  def keyPressed(self, key, unicode):
    if not self.songs or self.accepted:
      return

    c = self.engine.input.controls.getMapping(key)
    if c in [Player.KEY1] or key == pygame.K_RETURN:
      self.engine.view.popLayer(self)
      self.accepted = True
      if not self.song:
        self.engine.data.acceptSound.play()
    elif c in [Player.CANCEL, Player.KEY2]:
      self.selectedSong = None
      self.engine.view.popLayer(self)
      self.accepted = True
      if not self.song:
        self.engine.data.cancelSound.play()
    elif c in [Player.UP, Player.ACTION1]:
      self.selectedIndex = (self.selectedIndex - 1) % len(self.songs)
      self.updateSelection()
      if not self.song:
        self.engine.data.selectSound.play()
    elif c in [Player.DOWN, Player.ACTION2]:
      self.selectedIndex = (self.selectedIndex + 1) % len(self.songs)
      self.updateSelection()
      if not self.song:
        self.engine.data.selectSound.play()
    return True

  def songLoaded(self, song):
    self.songLoader = None

    if self.song:
      self.song.stop()
    
    song.setGuitarVolume(1.0)
    song.play()

  def playSelectedSong(self):
    song = self.getSelectedSong()
    if not song:
      return
    
    if self.songLoader:
      self.songLoader.cancel()
      # Don't start a new song loader until the previous one is finished
      if self.songLoader.isAlive():
        self.songCountdown = 256
        return

    if self.song:
      self.song.fadeout(1000)
      self.song = None

    self.songLoader = self.engine.resource.load(self, "song", lambda: Song.loadSong(self.engine, song, playbackOnly = True),
                                                onLoad = self.songLoaded)
    
  def run(self, ticks):
    self.time += ticks / 50.0

    if self.songCountdown > 0:
      self.songCountdown -= ticks
      if self.songCountdown <= 0:
        self.playSelectedSong()

    d = self.cameraOffset - self.selectedOffset
    self.cameraOffset -= d * ticks / 192.0
    
    for i in range(len(self.cassetteAngles)):
      if i == self.selectedIndex:
        self.cassetteAngles[i] = min(90, self.cassetteAngles[i] + ticks / 2.0)
      else:
        self.cassetteAngles[i] = max(0,  self.cassetteAngles[i] - ticks / 2.0)
    
  def renderCassette(self, index):
    if not self.cassette:
      return

    color = self.songs[index][1].cassetteColor
    if color:
      glColor3f(*color)

    glEnable(GL_COLOR_MATERIAL)
    glRotatef(90, 0, 1, 0)
    self.cassette.render("Mesh_001")
    glColor3f(.1, .1, .1)
    glScalef(-1, 1, 1)
    self.cassette.render("Mesh")

    # Draw the label if there is one
    if self.cassetteLabels[index] is not None:
      glEnable(GL_TEXTURE_2D)
      self.cassetteLabels[index].bind()
      glColor3f(1, 1, 1)
      glMatrixMode(GL_TEXTURE)
      glScalef(-1, -1, 1)
      glMatrixMode(GL_MODELVIEW)
      self.label.render("Mesh_001")
      glMatrixMode(GL_TEXTURE)
      glScalef(-1, -1, 1)
      glMatrixMode(GL_MODELVIEW)
      glDisable(GL_TEXTURE_2D)
  
  def render(self, visibility, topMost):
    v = (1 - visibility) ** 2

    # render the background    
    t = self.time / 100
    w, h, = self.engine.view.geometry[2:4]
    r = .5
    self.background.transform.reset()
    self.background.transform.translate(v * 2 * w + w / 2 + math.sin(t / 2) * w / 2 * r, h / 2 + math.cos(t) * h / 2 * r)
    self.background.transform.rotate(-t)
    self.background.transform.scale(math.sin(t / 8) + 2, math.sin(t / 8) + 2)
    self.background.draw()
      
    # render the cassette list
    try:
      glMatrixMode(GL_PROJECTION)
      glPushMatrix()
      glLoadIdentity()
      gluPerspective(60, self.engine.view.aspectRatio, 0.1, 1000)
      glMatrixMode(GL_MODELVIEW)
      glLoadIdentity()
      
      glEnable(GL_DEPTH_TEST)
      glDisable(GL_CULL_FACE)
      glDepthMask(1)
      
      offset = 10 * (v ** 2)
      self.camera.origin = (-10 + offset, -self.cameraOffset, 4   + offset)
      self.camera.target = (  0 + offset, -self.cameraOffset, 2.5 + offset)
      self.camera.apply()
      
      y = 0.0
      for i, song in enumerate(self.songs):
        song, info = song
          
        c = math.sin(self.cassetteAngles[i] * math.pi / 180)
        h = c * self.cassetteWidth + (1 - c) * self.cassetteHeight
        
        d = (y + h * .5 + self.camera.origin[1]) / (4 * (self.camera.target[2] - self.camera.origin[2]))

        if i == self.selectedIndex:
          self.selectedOffset = y + h / 2
          Theme.setSelectedColor()
        else:
          Theme.setBaseColor()
          
        glTranslatef(0, -h / 2, 0)
        
        glPushMatrix()
        glRotate(self.cassetteAngles[i], 0, 0, 1)
        if abs(d) < 1.0:
          self.renderCassette(i)
        glPopMatrix()
        
        glTranslatef(0, -h / 2, 0)
        y += h
      glDisable(GL_DEPTH_TEST)
      glDisable(GL_CULL_FACE)
      glDepthMask(0)
      
    finally:
      glMatrixMode(GL_PROJECTION)
      glPopMatrix()
      glMatrixMode(GL_MODELVIEW)
    
    # render the song info
    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font
    
    try:
      glEnable(GL_BLEND)
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
      glEnable(GL_COLOR_MATERIAL)
      Theme.setBaseColor(1 - v)

      if self.songLoader:
        font.render(_("Loading Preview..."), (.05, .7 + v), scale = 0.001)

      x = .6
      y = .15
      font.render(self.prompt, (x, .05 - v))

      Theme.setSelectedColor(1 - v)
      
      name, info = self.songs[self.selectedIndex]
      angle = self.cassetteAngles[self.selectedIndex]
      f = ((90.0 - angle) / 90.0) ** 2
      pos = wrapText(font, (x, y), info.name, visibility = f, scale = 0.0016)
      Theme.setBaseColor(1 - v)
      wrapText(font, (x, pos[1] + font.getHeight() * 0.0016), info.artist, visibility = f, scale = 0.0016)

      Theme.setSelectedColor(1 - v)
      scale = 0.0011
      w, h = font.getStringSize(self.prompt, scale = scale)
      x = .6
      y = .5 + f / 2.0
      if len(info.difficulties) > 3:
        y = .42 + f / 2.0
        
      for d in info.difficulties:
        scores = info.getHighscores(d)
        if scores:
          score, stars, name = scores[0]
        else:
          score, stars, name = "---", 0, "---"
        Theme.setBaseColor(1 - v)
        font.render(unicode(d),     (x, y),           scale = scale)
        font.render(unicode(Data.STAR2 * stars + Data.STAR1 * (5 - stars)), (x, y + h), scale = scale * .9)
        Theme.setSelectedColor(1 - v)
        font.render(unicode(score), (x + .15, y),     scale = scale)
        font.render(name,       (x + .15, y + h),     scale = scale)
        y += 2 * h + f / 4.0
      
    finally:
      self.engine.view.resetProjection()

class FileChooser(BackgroundLayer, KeyListener):
  """File choosinglayer."""
  def __init__(self, engine, masks, path, prompt = ""):
    self.masks          = masks
    self.path           = path
    self.prompt         = prompt
    self.engine         = engine
    self.accepted       = False
    self.selectedFile   = None
    self.time           = 0.0
    self.menu           = None

    self.engine.loadSvgDrawing(self, "background", "editor.svg")

  def _getFileCallback(self, fileName):
    return lambda: self.chooseFile(fileName)

  def _getFileText(self, fileName):
    f = os.path.join(self.path, fileName)
    if fileName == "..":
      return _("[Parent Folder]")
    if os.path.isdir(f):
      return _("%s [Folder]") % fileName
    return fileName

  def getFiles(self):
    files = [".."]
    for fn in os.listdir(self.path):
      if fn.startswith("."): continue
      f = os.path.join(self.path, fn)
      for mask in self.masks:
        if fnmatch.fnmatch(fn, mask):
          break
      else:
        if not os.path.isdir(f):
          continue
      files.append(fn)
    files.sort()
    return files

  def updateFiles(self):
    if self.menu:
      self.engine.view.popLayer(self.menu)
    self.menu = Menu(self.engine, choices = [(self._getFileText(f), self._getFileCallback(f)) for f in self.getFiles()], onClose = self.close, onCancel = self.cancel)
    self.engine.view.pushLayer(self.menu)

  def chooseFile(self, fileName):
    path = os.path.abspath(os.path.join(self.path, fileName))
    if os.path.isdir(path):
      self.path = path
      self.updateFiles()
      return
    self.selectedFile = path
    accepted = True
    self.engine.view.popLayer(self.menu)
    self.engine.view.popLayer(self)
    self.menu = None
    
  def cancel(self):
    self.accepted = True
    self.engine.view.popLayer(self)

  def close(self):
    if not self.menu:
      self.accepted = True
      self.engine.view.popLayer(self)
    
  def shown(self):
    self.updateFiles()
    
  def getSelectedFile(self):
    return self.selectedFile
  
  def run(self, ticks):
    self.time += ticks / 50.0
    
  def render(self, visibility, topMost):
    v = (1 - visibility) ** 2

    # render the background    
    t = self.time / 100
    w, h, = self.engine.view.geometry[2:4]
    r = .5
    self.background.transform.reset()
    self.background.transform.translate(v * 2 * w + w / 2 + math.sin(t / 2) * w / 2 * r, h / 2 + math.cos(t) * h / 2 * r)
    self.background.transform.rotate(-t)
    self.background.transform.scale(math.sin(t / 8) + 2, math.sin(t / 8) + 2)
    self.background.draw()
      
    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font
    
    try:
      glEnable(GL_BLEND)
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
      glEnable(GL_COLOR_MATERIAL)
      Theme.setBaseColor(1 - v)
      wrapText(font, (.1, .05 - v), self.prompt)
    finally:
      self.engine.view.resetProjection()

class ItemChooser(BackgroundLayer, KeyListener):
  """Item menu layer."""
  def __init__(self, engine, items, selected = None, prompt = ""):
    self.prompt         = prompt
    self.engine         = engine
    self.accepted       = False
    self.selectedItem   = None
    self.time           = 0.0
    self.menu = Menu(self.engine, choices = [(c, self._callbackForItem(c)) for c in items], onClose = self.close, onCancel = self.cancel)
    if selected and selected in items:
      self.menu.selectItem(items.index(selected))
    self.engine.loadSvgDrawing(self, "background", "editor.svg")
    
  def _callbackForItem(self, item):
    def cb():
      self.chooseItem(item)
    return cb
    
  def chooseItem(self, item):
    self.selectedItem = item
    accepted = True
    self.engine.view.popLayer(self.menu)
    self.engine.view.popLayer(self)
    
  def cancel(self):
    self.accepted = True
    self.engine.view.popLayer(self)

  def close(self):
    self.accepted = True
    self.engine.view.popLayer(self)
    
  def shown(self):
    self.engine.view.pushLayer(self.menu)
    
  def getSelectedItem(self):
    return self.selectedItem
  
  def run(self, ticks):
    self.time += ticks / 50.0
    
  def render(self, visibility, topMost):
    v = (1 - visibility) ** 2

    # render the background    
    t = self.time / 100
    w, h, = self.engine.view.geometry[2:4]
    r = .5
    self.background.transform.reset()
    self.background.transform.translate(v * 2 * w + w / 2 + math.sin(t / 2) * w / 2 * r, h / 2 + math.cos(t) * h / 2 * r)
    self.background.transform.rotate(-t)
    self.background.transform.scale(math.sin(t / 8) + 2, math.sin(t / 8) + 2)
    self.background.draw()
      
    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font
    
    try:
      glEnable(GL_BLEND)
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
      glEnable(GL_COLOR_MATERIAL)
      Theme.setBaseColor(1 - v)
      wrapText(font, (.1, .05 - v), self.prompt)
    finally:
      self.engine.view.resetProjection()
      
      
class BpmEstimator(Layer, KeyListener):
  """Beats per minute value estimation layer."""
  def __init__(self, engine, song, prompt = ""):
    self.prompt         = prompt
    self.engine         = engine
    self.song           = song
    self.accepted       = False
    self.bpm            = None
    self.time           = 0.0
    self.beats          = []
    
  def shown(self):
    self.engine.input.addKeyListener(self, priority = True)
    self.song.play()
  
  def hidden(self):
    self.engine.input.removeKeyListener(self)
    self.song.fadeout(1000)
    
  def keyPressed(self, key, unicode):
    if self.accepted:
      return True
      
    c = self.engine.input.controls.getMapping(key)
    if key == pygame.K_SPACE:
      self.beats.append(self.time)
      if len(self.beats) > 12:
        diffs = [self.beats[i + 1] - self.beats[i] for i in range(len(self.beats) - 1)]
        self.bpm = 60000.0 / (sum(diffs) / float(len(diffs)))
        self.beats = self.beats[-12:]
    elif c in [Player.CANCEL, Player.KEY2]:
      self.engine.view.popLayer(self)
      self.accepted = True
      self.bpm      = None
    elif c in [Player.KEY1] or key == pygame.K_RETURN:
      self.engine.view.popLayer(self)
      self.accepted = True
      
    return True
  
  def run(self, ticks):
    self.time += ticks
    
  def render(self, visibility, topMost):
    v = (1 - visibility) ** 2

    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font
    
    fadeScreen(v)
          
    try:
      glEnable(GL_BLEND)
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
      glEnable(GL_COLOR_MATERIAL)
      Theme.setBaseColor(1 - v)
      wrapText(font, (.1, .2 - v), self.prompt)
      
      if self.bpm is not None:
        Theme.setSelectedColor(1 - v)
        wrapText(font, (.1, .5 + v),  _("%.2f beats per minute") % (self.bpm))
    finally:
      self.engine.view.resetProjection()
      
class KeyTester(Layer, KeyListener):
  """Keyboard configuration testing layer."""
  def __init__(self, engine, prompt = ""):
    self.prompt         = prompt
    self.engine         = engine
    self.accepted       = False
    self.time           = 0.0
    self.controls       = Player.Controls()
    self.fretColors     = [(0, 1, 0), (1, 0, 0), (1, 1, 0), (0, 0, 1), (1, 0, 1)]
    
  def shown(self):
    self.engine.input.addKeyListener(self, priority = True)
  
  def hidden(self):
    self.engine.input.removeKeyListener(self)
    
  def keyPressed(self, key, unicode):
    if self.accepted:
      return True

    self.controls.keyPressed(key)
    c = self.engine.input.controls.getMapping(key)
    if c in [Player.CANCEL]:
      self.engine.view.popLayer(self)
      self.accepted = True
    return True

  def keyReleased(self, key):
    self.controls.keyReleased(key)
  
  def run(self, ticks):
    self.time += ticks
    
  def render(self, visibility, topMost):
    v = (1 - visibility) ** 2

    self.engine.view.setOrthogonalProjection(normalize = True)
    font = self.engine.data.font
    
    fadeScreen(v)
          
    try:
      glEnable(GL_BLEND)
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
      glEnable(GL_COLOR_MATERIAL)
      Theme.setBaseColor(1 - v)
      wrapText(font, (.1, .2 - v), self.prompt)

      for n, c in enumerate(Guitar.KEYS):
        if self.controls.getState(c):
          glColor3f(*self.fretColors[n])
        else:
          glColor3f(.4, .4, .4)
        font.render("#%d" % (n + 1), (.5 - .15 * (2 - n), .4 + v))

      if self.controls.getState(Player.ACTION1) or \
         self.controls.getState(Player.ACTION2):
        Theme.setSelectedColor(1 - v)
      else:
        glColor3f(.4, .4, .4)
      font.render(_("Pick!"), (.45, .5 + v))
        
    finally:
      self.engine.view.resetProjection()
      
def _runDialog(engine, dialog):
  """Run a dialog in a sub event loop until it is finished."""
  if not engine.running:
    return
  
  engine.view.pushLayer(dialog)

  while engine.running and dialog in engine.view.layers:
    engine.run()

def getText(engine, prompt, text = ""):
  """
  Get a string of text from the user.
  
  @param engine:  Game engine
  @param prompt:  Prompt shown to the user
  @param text:    Default text
  """
  d = GetText(engine, prompt, text)
  _runDialog(engine, d)
  return d.text

def getKey(engine, prompt, key = None):
  """
  Ask the user to choose a key.
  
  @param engine:  Game engine
  @param prompt:  Prompt shown to the user
  @param key:     Default key
  """
  d = GetKey(engine, prompt, key)
  _runDialog(engine, d)
  return d.key

def chooseSong(engine, prompt = _("Choose a Song"), selectedSong = None):
  """
  Ask the user to select a song.
  
  @param engine:        Game engine
  @param prompt:        Prompt shown to the user
  @param selectedSong:  Name of song to select initially
  """
  d = SongChooser(engine, prompt, selectedSong = selectedSong)
  _runDialog(engine, d)
  return d.getSelectedSong()
  
def chooseFile(engine, masks = ["*.*"], path = ".", prompt = _("Choose a File")):
  """
  Ask the user to select a file.
  
  @param engine:  Game engine
  @param masks:   List of glob masks for files that are acceptable
  @param path:    Initial path
  @param prompt:  Prompt shown to the user
  """
  d = FileChooser(engine, masks, path, prompt)
  _runDialog(engine, d)
  return d.getSelectedFile()
  
def chooseItem(engine, items, prompt, selected = None):
  """
  Ask the user to one item from a list.
  
  @param engine:    Game engine
  @param items:     List of items
  @param prompt:    Prompt shown to the user
  @param selected:  Item selected by default
  """
  d = ItemChooser(engine, items, prompt = prompt, selected = selected)
  _runDialog(engine, d)
  return d.getSelectedItem()
  
def testKeys(engine, prompt = _("Play with the keys and press Escape when you're done.")):
  """
  Have the user test the current keyboard configuration.
  
  @param engine:  Game engine
  @param prompt:  Prompt shown to the user
  """
  d = KeyTester(engine, prompt = prompt)
  _runDialog(engine, d)
  
def showLoadingScreen(engine, condition, text = _("Loading..."), allowCancel = False):
  """
  Show a loading screen until a condition is met.
  
  @param engine:      Game engine
  @param condition:   A function that will be polled until it returns a true value
  @param text:        Text shown to the user
  @type  allowCancel: bool
  @param allowCancel: Can the loading be canceled
  @return:            True if the condition was met, Fales if the loading was canceled.
  """
  
  # poll the condition first for some time
  n = 0
  while n < 32:
    n += 1
    if condition():
      return True
    engine.run()

  d = LoadingScreen(engine, condition, text, allowCancel)
  _runDialog(engine, d)
  return d.ready

def showMessage(engine, text):
  """
  Show a message to the user.
  
  @param engine:  Game engine
  @param text:    Message text
  """
  Log.notice("%s" % text)
  d = MessageScreen(engine, text)
  _runDialog(engine, d)

def estimateBpm(engine, song, prompt):
  """
  Ask the user to estimate the beats per minute value of a song.
  
  @param engine:  Game engine
  @param song:    Song instance
  @param prompt:  Prompt shown to the user
  """
  d = BpmEstimator(engine, song, prompt)
  _runDialog(engine, d)
  return d.bpm
  
