# vim:ts=4:autoindent
require 'yaml'
require 'phrases'
require 'Log'

PFX = "1"

#
# Wheel of Words
#
# Game Events:
#
# INITIALIZATION:
#
# 1.  The player array, of people who are playing (@playing) initialized and empty.
# 2.  Game started (@started) is FALSE.
# 3.  Whose turn is it (@turn) is NIL.
# 4.  Read in high scores.  XXX Create if not on disk.
# 5.  Register with ddial to receive private message events to our "virtual line"
#     (in this case, PFX is line 1).
#
# WAITING FOR PLAYERS:
#
# As people "join", the player array (@playing) gets a 3-element array appended to it.
# element[0] - the LINKLINE of the user (player) for easy id purposes
# element[1] - the User object of the player
# element[2] - initialized to 0, the running total of the player's score
# NEW  element[0] is now just USER object   element[1] is players score
# START GAME:
#
# When the bot receives the "start" message the start_game function is called.
# 1.  @started is set to TRUE
# 2.  @turn is initialized to 0, and will be used to step through the @playing array
#     one by one.
# 3.  @letters_taken is initialized to an empty array.  XXX should be in Initialization, and emptied to save memory
# 4.  random_phrase function is called, to pick a random phrase.
#     1.  PHRASES is a constant of an Array which is read from phrases.rb, and consists of a
#         two-element array - element[0] - the Category (placed in @category)
#                             element[1] - the Phrase (placed in @real_phrase)
#     2.  @phrase is the real phrase, with = subbed in for letters.
# 5.  random_points function is called
#     it sets the amount of @points this turn is worth.
#
# GAME PLAY:
# A letter is guessed.  It is added to @letters_taken, a count is made of the matches in @real_phrase,
# the letters are revealed in @phrase.  If there are no more = left in phrase, XXX (cannot have = in a phrase!)
# the game is over.  A player can also try to guess the whole phrase.
#
# GAME END:
# 1.  winner is selected, and compared against @high_scores.  Make & Save high score table if new high score.
# 2.  @playing is cleared.
# 3.  Game @started = FALSE.
# 4.  @turn initialized to 0.
# 5.  @letters_taken is cleared.


class Array
    def randomize
        duplicated_original, new_array = self.dup, self.class.new
        until new_array.size.eql?(self.size)
            new_array << duplicated_original.slice!(rand(duplicated_original.size))
        end
        new_array
    end

    def randomize!
        self.replace randomize
    end
end


class WheelOfWords
    def initialize(dd)
        @dd = dd

        @playing = []
        @started = false
        @turn = nil
        @letters_taken = []

        File.open( "wow-high-scores.yaml" ){ |yf| @high_scores = YAML.load(yf) }

        @dd.on_event( self, :type => :privmsg, :priv_to => 1 ){ |ev| did_priv(ev) }
    end

    def random_points
        @points = (rand(10) + 1) * 1000
    end

    def random_phrase
        picked = rand(PHRASES.length)
        @category = PHRASES[picked][0]
        @real_phrase = PHRASES[picked][1]
        @phrase = @real_phrase.gsub(/[A-Za-z]/, "=")
    end

    def reset_variables
        @playing.clear
        @started = false
        @turn = 0
        @letters_taken.clear
    end

    def start_game
        @started = true
        @turn = 0

        puts "@playing.length: #{@playing.length}"
        @playing.randomize!
        puts "********** NOW @playing.length: #{@playing.length}"
        random_phrase
        random_points
    end

    def end_game
        winner =    if @playing.length == 1: @playing[0]
                    else                     @playing[@turn]
                    end

        match = nil
        catch(:done) do
            @high_scores.each_with_index do |hs, i|
                if winner[1] >= hs[1]
                    match = i
                    throw :done
                end
            end
        end

        if !match.nil?
            @high_scores.insert(match, [ winner[0].llha, winner[1] ])
            @high_scores.delete_at(5)

            @dd.write( "#{PFX}^WoW: New High Score!!  #{winner[0].llha} scored #{winner[1]} points!\r" )
            File.delete("wow-high-scores.yaml")
            File.open("wow-high-scores.yaml", "w"){ |yf| YAML.dump(@high_scores, yf) }
        end

        reset_variables
    end

    HELP = [
"^Wheel of Words!^^Commands:^help - list help about Wheel of Words!^join - Sign up for a game (must be at least \
2 players signed up for a game to play).^status - Status of current game^high - list high scores.\r",
"^left - Sends you what letters are left.^quit - Quits a game you have joined.^start - Start a game after the \
players have signed up.^all <guess> - Guess the whole answer.^score - list joined players and their scores.\r",
"^Once a game has started, you can guess a letter by typing \"/P# x\", where x is the letter you wish to guess. \
You lose your turn when you guess a letter that doesn't exist, or when you guess a vowel.\r" ]

    def do_help(u)
        HELP.each{ |line| u.privmsg " #{PFX}" + line }
    end

    def do_high(u)
        msg = [ " #{PFX}WoW: High scores" ]
        @high_scores.each{ |name, score| msg << "^#{name} - #{score}" }
        u.privmsg(msg.join + "\r")
    end

    def do_quit(u)
        return if game_not_in_progress(u)

        is_index = nil
        @playing.each_with_index{ |p, i| is_index = i if p[0] == u }

        if is_index.nil?
            u.privmsg " #{PFX}WoW: You're not playing already?\r"
            return
        end

        @dd.write( "#{PFX}^WoW: #{u.llha} has quit the game" )

        @playing.delete_at(is_index)
        if @playing.length > 1
            # Still 2 or more players left - adjust and finish game.
            if @turn > is_index
                @turn -= 1
                @turn = (@playing.length-1) if @turn < 0 # XXX should never happen
            end
            @dd.write( ".^" )
            update_display
        else
            # Otherwise, forfeit game to remaining player.
            last = @playing[0][0]

            @dd.write( ", leaving #{last.llha} as the only player left who wins" )
            @dd.write( " by default with #{@playing[0][1]} points!^Answer was: #{@real_phrase}\r" )
            end_game
        end
    end

    def do_status(u)
        if !@started
            msg = [ " #{PFX}WoW: Game not started.  " ]
            if @playing.length == 0
                msg << "No players have joined yet.\r"
            else
                msg << "Waiting to play:"
                @playing.each{ |u, score| msg << "^#{u.llha}" }
            end
            u.privmsg( msg.join + "\r" )
        else
            @dd.write( "/P#{u.line} #{PFX}" )   # XXX legal, but a hack, change to privmsg?
            update_display
        end
    end

    def do_left(u)
        return if game_not_in_progress(u)

        left = ""
        p @letters_taken
        0.upto(25){ |n| left << n+65 if @letters_taken[n] == nil }
        u.privmsg " #{PFX}WoW: Letters left: #{left}\r"
    end

    def do_guess_all(u, guess)
        return if game_not_in_progress(u)
        return if not_your_turn(u)

        if @real_phrase.downcase.eql?(guess.downcase)
            @dd.write( "#{PFX}^WoW: #{u.llha} guessed \"#{@real_phrase}\" as the answer and was right!  Winning the game with #{@playing[@turn][1]} points!^Game over.\r" )
            end_game
        else
            @dd.write( "#{PFX}^WoW: #{u.llha} guessed \"#{guess}\" as the answer and was wrong.\r" )
            next_player
        end
    end

    def do_join(u)
        return if game_in_progress(u)
        return if already_joined(u)

        @playing << [ u, 0 ]

        @dd.write( "#{PFX}^WoW: #{u.llha} has joined the game.\r" )
    end

    def do_score(u)
        return if game_not_in_progress(u)

        msg = [ " #{PFX}^Scores:" ]
        @playing.each{ |u, score| msg << "^#{u.llha} - #{score}" }
        u.privmsg( msg.join + "\r" )
    end

    def do_start(u)
        return if game_in_progress(u)

        if @playing.length <= 1
            u.privmsg " #{PFX}WoW: Cannot start a game with less than 2 players.\r"
            return
        end

        @dd.write( "#{PFX}^Wheel of Words game started by #{u.llha}^" )  # update_display will \r
        start_game
        update_display
    end

    def do_guess_letter(u, guess)
        return if game_not_in_progress(u)
        return if not_your_turn(u)
        return if reject_character(u, guess)
        return if letter_already_taken(u, guess)

        @letters_taken[guess-65] = true

        match = 0
        0.upto(@real_phrase.length-1) do |l|
            if @real_phrase[l,1].upcase[0] == guess
                @phrase[l] = @real_phrase[l]
                match += 1
            end
        end

        if match == 0
            @dd.write( "#{PFX}^There are no #{"%c" % guess}'s!^" )      # update_display will \r
            next_player
        else
            tpoints = @points * match
            @dd.write( "#{PFX}^There #{match == 1 ? "is" : "are"} #{match} #{"%c" % guess}'s! #{tpoints} points!^" )
            @playing[@turn][1] += tpoints

            if (/=/ =~ @phrase).nil?
                @dd.write("\r#{PFX}^WoW: #{u.llha} has guessed the last correct letter!, winning with #{@playing[@turn][1]} points!^Game over.\r" )
                end_game
                return
            end
            [ "A", "E", "I", "O", "U" ].each{ |vowel| next_player if vowel[0,1][0] == guess }
        end
        random_points
        update_display
    end

    def do_reset(u)
        if !u.is_hat
            u.privmsg " #{PFX}WoW: I can be reset, but only if you have a hat.\r"
            return
        end

        @dd.write( "#{PFX}^WoW: Game data reset by #{u.llha}.\r" )
        reset_variables
    end

    def did_priv(ev)
        u = ev[:user]
        if u.nil?
            @dd.write( "#{PFX}^WoW: Unable to process #{ev[:handle]}.\r" )
            return
        end

        if ev[:message] == "help":          do_help(u)
        elsif ev[:message] == "high":       do_high(u)
        elsif ev[:message] == "quit":       do_quit(u)
        elsif ev[:message] == "status":     do_status(u)
        elsif ev[:message] == "left":       do_left(u)
        elsif ev[:message] == "join":       do_join(u)
        elsif ev[:message] == "score":      do_score(u)
        elsif ev[:message] == "start":      do_start(u)
        elsif ev[:message] == "reset":      do_reset(u)

        elsif not (md = /all /i.match(ev[:message])).nil?
            do_guess_all(u, md.post_match)

        elsif ev[:message].chomp.length == 1
            do_guess_letter(u, ev[:message][0,1].upcase[0])

        else
            u.privmsg " #{PFX}WoW: Unknown command, enter \"/P# help\" for help on playing Wheel of Words!\r"
        end

    end

    def next_player
        @turn += 1
        @turn = 0 if @turn >= @playing.length

        random_points
    end

    def update_display
        @dd.write( "^WoW!^Turn: #{@playing[@turn][0].llha}^Value: *> #{@points} Points! <*^Category: #{@category}^Clue: #{@phrase}\r" )
    end

    def game_not_in_progress(u)
        if !@started
            u.privmsg " #{PFX}WoW: Game is not in progress.\r"
            return true
        end

        false
    end

    def game_in_progress(u)
        if @started
            u.privmsg " #{PFX}WoW: Game has already started.  Wait for next game.\r"
            return true
        end

        false
    end

    def already_joined(u)
        has = false
        @playing.each{ |user, score| has = true if user == u }

        if has
            u.privmsg " #{PFX}WoW: You have already joined.\r"
            return true
        end

        false
    end

    def not_your_turn(u)
        if @playing[@turn][0] != u
            u.privmsg " #{PFX}WoW: It's not your turn.\r"
            return true
        end

        false
    end

    def reject_character(u, guess)
        if guess < 65 || guess > 90
            u.privmsg " #{PFX}WoW: #{"%c" % guess} not a supported character.\r"
            return true
        end

        false
    end

    def letter_already_taken(u, guess)
        if @letters_taken[guess-65] == true
            u.privmsg " #{PFX}WoW: #{"%c" % guess} is already taken!\r"
            return true
        end

        false
    end
end