#!/usr/bin/ruby

CNT = 3
EIGHTS = 1	# Counting 8's instead of beats
SKIP = 40

require 'delegate'

##################################################
# Updates to common methods
##################################################
class String
	# I'm surprised that ruby doesn't have this built in for system calls
	@@quote_chars = %w(\\ & ; | * < > \( \) ' ") + [' ']
	def quote!
		@@quote_chars.each { |q|
			self.gsub!(q) { |g| "\\"+g }
		}
		self
	end
	def quote
		new = self.dup
		new.quote!
	end
end

##################################################
# Song handlers (mp3/ogg/..)
##################################################
class SongMP3
	@@tool = "id3tool"
	# A frame is about 3200bytes?  So that's ~40 frames/s at 128kb/s
	@@play = %w(mpg123 --loop -1 -k)+[(SKIP*40).to_s]
	@path = ''
	@playing = nil
	def initialize(path)
		@path = path
	end
	def comment
		c = @@tool+" "+@path.quote
		comment = ''
		IO.popen(c).each { |l|
			comment += $1 if l =~ /^Note:\s+(\S.*?)\s*$/
		}
		comment
	end
	def comment=(val)
		c = @@tool+" "+@path.quote+" -n "+val.quote
		#puts "SET #{c}"
		raise RuntimeError, "Trouble running #{@@tool}" unless system(c)
	end
	def play
		@playing = fork
		return if @playing
		$stdout.close()
		$stderr.close()
		$stdin.close()
		exec(*@@play+[@path])
		exit	# Shouldn't get here
	end
	def stop
		return unless @playing
		Process.kill(9,@playing)
	end

	def self.is(path)
		return path =~ /\.mp3$/i ? true : false
	end
end

class SongOGG
	@@tool = "vorbiscomment"
	@@play = %w(ogg123 -k)+[SKIP.to_s]
	@path = ''
	@playing = nil
	@tags = nil
	def initialize(path)
		@path = path
	end
	def comment
		c = @@tool+" "+@path.quote
		@tags = Hash.new
		IO.popen(c).each { |l|
			@tags[$1] = $2 if l =~ /^([^=]+)=(.+)/
		}
		@tags['comment']	# There's a description tag, but xmms uses 'comment'
	end
	def comment=(val)
		comment unless @tags
		@tags['comment'] = val
		c = @@tool+" -w "+@path.quote
		@tags.each_pair { |k,v|
			c += " -t #{k}=#{v.quote}"
		}
		#puts c
		raise RuntimeError, "Trouble running #{@@tool}" unless system(c)
		#puts "SYS #{c} gave #{$?}"
	end
	def play
		@playing = fork
		return if @playing
		$stdout.close()
		$stderr.close()
		$stdin.close()
		exec(*@@play+[@path])
		puts "YO!"
		exit	# Shouldn't get here
	end
	def stop
		return unless @playing
		Process.kill(9,@playing)
	end

	def self.is(path)
		return path =~ /\.ogg$/i ? true : false
	end
end

# Delegate to the right song object
class Song < SimpleDelegator
	attr_reader :name, :path
	@actual = nil
	def initialize(path)
		@path = path
		@name = path.sub(/.*\//,'')
		case true
		when SongMP3.is(path)
			@actual = SongMP3.new(path)
		when SongOGG.is(path)
			@actual = SongOGG.new(path)
		else
			raise RuntimeError, "Unknown song type:\n  #{path}\n"
		end
		__setobj__(@actual)	# Delegate
	end
end

##################################################
# BPM code
##################################################
def getBPM
	times = Array.new
	puts "\nHit return for every " + (EIGHTS ? "8 beats" : "beat") + "\n  'b' for back, 's' for skip, 'r' for retry, 'q' for quit"
	CNT.times { |i|
		print "#{CNT-i} "
		g = $stdin.gets.chomp
		return false if g =~ /q/;
		return -1 if g =~ /b/;
		return -2 if g =~ /s/;
		return $1.to_i if g =~ /=(\d+)/;
		if (g =~ /r/) then
			puts "  Try again.."
			return getBPM
		end
		times.push Time.now
	}

	ave = 0
	(CNT-1).times { |t|
		ave += times[t+1]-times[t]
	}
	# We really only need to look at the first and last time, but whatever..
	ave /= (CNT-1)
	ave /= 8 if EIGHTS
	(60/ave).to_i
end

##################################################
# Manage songs
##################################################
# Figure out which songs already have BPM
def usage
	puts "Usage: #{$0.sub(/.*\//,'')} <songs>"
	puts "  Tag songs with BPMs (in comments)"
	puts
	puts "  -f      Force tagging, even if song already has BPMs"
	puts "  -clear  Clear any prior comments"
	puts
	exit -1
end

def songList
	songs = Array.new
	force = false
	tmpforce = false
	clear = false

	usage if ARGV.empty?

	ARGV.each { |path|
		case path
		when '-h'
			usage
		when '-?'
			usage
		when '-f' 
			force = true
		when '-clear'
			clear = true
		else
			songs.push(path)
		end
	}

	count = 0
	loop {
		count += 1
		break if count > songs.size

		path = songs[count-1]
		song = Song.new(path)

		c = song.comment
		if (!tmpforce && !force && c =~ /(\d+)bpm/) then
			# Already tagged
			puts "Tagged (#{$1} bpm): #{song.name}"
			next
		end
		tmpforce = false
		c = "" if c =~ /^\s+$/
		c && c.gsub!(/^(\d+bpm\s*)+/,'')	# In case of force

		puts "Name: #{song.name} #{count}/#{songs.length}"
		puts "  comment: <#{c}>" if c && !c.empty?
		c= "" if clear

		song.play
		bpm = getBPM
		song.stop
		exit unless bpm
		if (bpm<0) then
			count -= 2 if bpm==-1
			tmpforce = true
			next
		end
		puts "BPM: #{bpm}"

		c = c && !c.empty? ? "#{bpm}bpm #{c}" : "#{bpm}bpm"
		song.comment=c
	}
end

#puts "#{bpm} BPM"

songList
