# Cddb module for Ruby.
# by jared jennings. liver@soon.com
# under Ruby's license http://www.ruby-lang.org/en/LICENSE.txt

# i don't really like this design. if you have better ideas
# please email me at liver@soon.com

# undocumented as of yet.
# look at the source. and wavcddb. :)

require 'audiofile'
require 'socket'
require 'timeout'

# some algorithms adapted from mp3cddb ...
# notice follows
#
# mp3cddb
# -------
# originally: mp3tocddb.pl by Meng Weng Wong (MPEG::MP3Info)
# modified by: <drewie@bigfoot.com>
#

module Cddb

    class Toc
        def initialize
            @filenames = []
            @titles = []
            @artists = []
            @discTitle = ""
            @discArtist = ""
            @category = ""
            @discID = ""
        end

        def prepare filenames
            @filenames = filenames

            @lengths = []
            filenames.each do |fn|
                fil = AudioFile.new fn
                @lengths.push (fil.frame_count*1000/fil.rate)
                fil.close
            end

            @nrTracks = @lengths.length

            offset = 150                   # first track offset, nearly always. 
            @frameOffsets = []
            @totalLength = 0
            n = 0
            @lengths.each do |length|
                @frameOffsets.push offset
                offset += (length * 75 / 1000)
                n += digitSum(length / 1000)
                @totalLength += length / 1000
            end

            @discID = sprintf("%08x", ((n % 0xff)<<24) | \
                                      (@totalLength << 8) | \
                                      @nrTracks)
        end


     
        def digitSum num
            result = 0
            while num > 0
                result += num % 10
                num /= 10
            end
            return result
        end


        def setFromCddbInfo lines
            maxTrackNo = 0
            @titles = []
            @artists = []
            lines.each do |line|
                if line =~ "TTITLE(.{1,2})=(.*)"
                    trackNo = $1.to_i
                    maxTrackNo = trackNo if trackNo > maxTrackNo
                    @titles[trackNo] = $2
                else
                    if line =~ "DTITLE=(.*) / (.*)"
                        @discArtist = $1
                        @discTitle = $2
                    else
                        if line =~ "#cddb read (.*) (.*)"
                            @category = $1
                            @discID = $2
                        end
                    end
                end
            end

            # split up track titles into artist + title as best we can

            @titles.each_index do |ind|
                if (@titles[ind] =~ /(.*?)\s*\/\s*(.*)/) 
                    @artists[ind] = $1
                    @titles[ind] = $2
                else
                    if (@titles[ind] =~ /(.*?)\s+-+\s+(.*)/)
                        @titles[ind] = $1
                        @artists[ind] = $2
                    else
                        @artists[ind] = nil
                    end
                end
            end

            nrTracks = maxTrackNo + 1
            if nrTracks != @nrTracks
                raise "Info from server has #{nrTracks} track titles, but we want #{@nrTracks}"
            end

            if !defined? @discArtist
                raise "No artist found for CD"
            end
            if !defined? @discTitle
                raise "No title found for CD"
            end
        end

        def inspect

            # puts out ruby code, so we can have the
            # beautiful short uninspect shown below

            string = ""

            string += "@discArtist=#{@discArtist.inspect}\n"
            string += "@discTitle=#{@discTitle.inspect}\n"
            string += "@artists = []\n"
            string += "@titles = []\n"
            string += "# (@artists[foo]=nil means use @discArtist)\n"
            string += "\n"

            @titles.each_index do |ind|
                indString = sprintf("%2d",ind)
                string += "  @artists[#{indString}]=#{@artists[ind].inspect}\n"
                string += "   @titles[#{indString}]=#{@titles[ind].inspect }\n"
                string += "\n"
            end

            string += "@discID=#{@discID.inspect}\n"
            string += "@category=#{@category.inspect}\n"
            string += "@totalLength=#{@totalLength}\n"
            string += "@nrTracks=#{@nrTracks}\n"

            string += "@lengths=#{@lengths.inspect}\n"
            string += "@frameOffsets=#{@frameOffsets.inspect}\n"
            string += "@filenames=#{@filenames.inspect}\n"

            return string
        end

        def uninspect inspectString
            # probably insecure. do not use where security is a must.
            # (i don't know about security in ruby yet.)

            self.instance_eval(inspectString)
        end



        def lengths 
            @lengths end
        def totalLength 
            @totalLength end
        def frameOffsets 
            @frameOffsets end
        def nrTracks 
            @nrTracks end
        def discID 
            @discID end
        def category
            @category end
        def filenames
            @filenames end
        def artists
            @artists end
        def titles
            @titles end
        def discArtist
            @discArtist end
        def discTitle
            @discTitle end

        def artists= newArtists
            @artists = newArtists
        end
        def titles= newTitles
            @titles = newTitles
        end
        def discArtist= newDiscArtist
            @discArtist = newDiscArtist
        end
        def discTitle= newDiscTitle
            @discTitle = newDiscTitle
        end
        def discID= newDiscID
            @discID = newDiscID
        end
        def category= newCat
            @category = newCat
        end
    end

           
    class Connection
        SOFTWARE_NAME = 'ruby-cddb'
        SOFTWARE_VERSION = '0.1'

        def initialize server,port
            @server = server
            @port = port
            @login = ENV['USER']
            @hostname = `hostname`.chomp
            print "Connecting to server #{server}:#{port}... "
            $stdout.flush
            @sock = TCPSocket.open server, port
            print " connected.\n"

                def @sock.getsWithTimeout secs
                    result = ""
                    begin
#                    $stdout.print "<<< "
#                    $stdout.flush
                        timeout(secs) {
                            result = self.gets
#                            $stdout.print result.chomp, "\n"
                        }
                    rescue TimeoutError
                        raise TimeoutError, "recv timed out after #{secs} seconds"
                    end
                    return result
                end

                def @sock.printWithTimeout secs, *stuff
                    result = ""
                    begin
#                        $stdout.print ">>> \n"
#                        $stdout.flush
                        timeout(secs) {
                            result = self.print(stuff)
#                            $stdout.print  stuff,"\n"
                        }
                    rescue TimeoutError
                        raise TimeoutError, "send timed out after #{secs} seconds"
                    end
                end


            banner = @sock.getsWithTimeout 30
            case banner[0..2].to_i
                when 432, 433, 434
                    @sock.close
                    raise "Connection refused from #{@server}: #{banner}"
                when 200
                    print "Handshake with #{@server}:#{@port}.\n"
                when 201
                    print "Handshake with #{@server}:#{@port} - read only.\n"
                    @readonly = true
                else
                    @sock.close
                    raise "Unknown response from server: #{banner}"
            end
            
            @sock.printWithTimeout 15,"cddb hello #{@login} #{@hostname}"\
                                      " #{SOFTWARE_NAME} #{SOFTWARE_VERSION}\n"
            response = @sock.getsWithTimeout 90
            case response[0..2].to_i
                when 200
                    ; #OK
                when 431
                    @sock.close
                    raise "Server doesn't want to shake hands; connection closed"
                when 402
                    print "Server is crazy; thinks we've already shaken hands. Going ahead.\n"
                when 500
                    print "Program error; contact ruby-cddb maintainer.\n"
                else 
                    @sock.close
                    raise "Unknown response from server: #{response}"
            end
        end

        def connected?
            @sock and !@sock.closed?
        end



        def query toc
            if not self.connected?
                raise "Query attempted while not connected to a server"
            end

            query = "cddb query #{toc.discID} #{toc.nrTracks} "
            toc.frameOffsets.each do |ofs|
                query += "#{ofs} "
            end
            query += "#{toc.totalLength}\n"
            print "> ",query.chomp, "\n"
            @sock.printWithTimeout 15, query
            response = @sock.getsWithTimeout 30
            print "< ",response.chomp("\r\n"), "\n"
            alldiscs = [ ]
            case response[0..2].to_i
                when 200
                    # Exact match found.
                    discinfo = response.split(" ")
                    discinfo.shift                # get rid of code @ beginning
                    alldiscs = [ discinfo ]
                when 211
                    # Inexact matches found; list follows until terminating '.'
                    until response == "." 
                        response = @sock.getsWithTimeout(15)
                        response.chomp!("\r\n")
                        thisinfo = response.split(" ")
                        alldiscs.push thisinfo
                    end
                    alldiscs.pop             # get rid of terminating '.'
                when 202
                    print "no match found.\n"
                when 403
                    print "match found, but corrupt.\n"
                when 409
                    print "no handshake! Server doesn't think we've connected properly\n"
                    raise "Unknown connection state"
            end
            return alldiscs
        end


        def read category,discID
            if not self.connected?
                raise "Read attempted while not connected to a server"
            end

            query = "cddb read #{category} #{discID}\n"
            print "> ",query.chomp,"\n"
            lines = [ "#"+query ]
            @sock.printWithTimeout 15, query
            response = @sock.getsWithTimeout 60
            case response[0..2].to_i
                when 210
                    $stdout.print "Reading #{category} #{discID} "
                    $stdout.flush
                    # OK. Entry follows until terminating '.'
                    line = ""
                    until line == "."
                        line = (@sock.getsWithTimeout 30).chomp "\r\n"
                        lines.push line unless line =~ "^#"
                        $stdout.print "."
                        $stdout.flush
                    end
                    lines.pop               # get rid of terminating '.'
                    $stdout.print " done.\n"
                else
                    raise "Unknown response #{response} from server"
            end
            return lines
        end

        def close
            if self.connected?
                @sock.close
            end
        end

    end

end
