#!/usr/bin/env ruby

require 'socket'

module Holdem
  class Player
    attr_accessor :name, :sock, :cash, :table, :cards, :flags
    
    def initialize(name, sock, cash)
      @name, @sock, @cash = name, sock, cash
      @table = nil
      @cards = nil
      @flags = {}
    end

    def send_error(str)
      @sock.puts "error #{str}"
    end

    def addr
      @sock.peeraddr[2]
    end

    def handle_cmd(str)
      raise "unknown cmd: #{str}"
    end
  end

  class Table
    attr_accessor :name, :desc, :rules, :dealer, :state #, :playing, :pot

    #
    # initialize new holdem table
    #
    def initialize(name, desc, rules)
      @name, @desc, @rules = name, desc, rules
      @players = []
      @dealer = 0
      @state = END_HAND
    end

    #
    # send game msg to all players at table
    #
    def send_to_all(str)
      @players.each { |p| p.sock.puts str }
    end


    #
    # add player to table
    #
    def add_player(player)
      @players << player
      send_to_all "tbl_add_ply \"#{player.name}\""
      player.table = self
    end

    #
    # remove player from table
    #
    def del_player(player)
      if @players.include? player
        @players -= player
        send_to_all "tbl_del_ply \"#{player.name}\""
        player.table = nil
      end
    end

    #
    # handle string from player
    #
    def handle_cmd(player, str)
      if str =~ /bet (.*)$/
        bet = $1.to_i
        if player.cash >= bet
          send_to_all("tbl_bet \"#{player.name}\" #{bet}")
        else
          player.send_error("You don't have enough cash to bet #{bet}.")
        end
      else
        raise "unknown cmd: \"#{str}\"."
      end
    end
  end

  class Server
    attr_accessor :host, :port, :players, :tables

    # 
    # create a new Holdem::Server on host:port
    #
    def initialize(host, port, opts = nil)
      @host, @port = host, port
      @opts = {
        :max_read         => 1024,
        :select_timeout   => 10.0,
        :msg_srv_info     => 'Holdem Server 1',
      }.update(opts || {})

      @sock = TCPServer.new(@host, @port)
      @players = []
      @tables = {}
    end

    #
    # find player by socket
    #
    def find_player(sock)
      @players.each { |p| return p if p.sock == sock }
      nil
    end

    #
    # send game msg to all players on server
    #
    def send_to_all(str)
      @players.each { |p| p.sock.puts str }
    end

    #
    # handle disconnect{ed,ing} player
    #
    def disconnect_player(player)
      player.table.del_player(player) if player.table
      @players -= [player]
      player.sock.close unless player.sock.closed?
      send_to_all("srv_discon \"#{player.name}\"")
    end

    #
    # handle server command from client
    # 
    def handle_cmd(player, str)
      if str =~ /srv_proto (.*)$/
        if $1 == '1'
          player.flags[:proto] = 1
        else
          player.send_error "Invalid protocol version \"#$1\"."
        end
      elsif str =~ /srv_name "(.*)"/
        name = $1.dup
        if !player.flags[:proto] 
          player.send_error 'Missing protocol version.'
        elsif name =~ /"/
          player.send_error 'Invalid characters in name.'
        else
          old_name = player.name
          player.name = name

          if player.flags[:named]
            send_to_all("srv_rename \"#{old_name}\" \"#{name}\"")
          else
            send_to_all("srv_connect \"#{name}\"")
            player.flags[:named] = true
          end
        end
      elsif str =~ /srv_join "(.*)"/
        name = $1.dup
        if tbl = @tables[name]
          # TODO: sanity check on user here
          tbl.add_player(player)
        else
          player.send_error "Unknown table \"#{name}\"."
        end
      elsif str =~ /srv_bye/
        disconnect_player(player)
      else
        raise "unknown cmd: \"#{str}\"."
      end
    end

    def dump_status(io = $stderr)
      title = 'Server Status'
      puts title, '-' * title.size 
      if @players.size > 0
        io.puts "Players:", @players.map { |p| '  ' << p.name }.join("\n")
      else
        io.puts 'No players.'
      end

      if @tables.size > 0
        io.puts "Tables:", @tables.map do |k, t|
          "  #{k} (#{t.players.size} players)"
        end.join("\n")
      else
        io.puts 'No tables.'
      end
    end

    #
    # check for and handle data from clients
    #
    def main_iteration
      timeout = @opts[:select_timeout]

      # build read array
      read_ary = []
      read_ary << @sock
      @players.each { |p| read_ary << p.sock }

      if ((ary = IO::select(read_ary, nil, nil, timeout)) && ary[0])
        ary[0].each do |sock| 
          if sock == @sock
            ply_sock = @sock.accept
            $stderr.puts "DEBUG: connection from #{ply_sock.peeraddr[2]}"
            if ply_sock 
              ply_sock.puts "srv_yo \"#{@opts[:msg_srv_info]}\""
              @players << Player::new('Unnamed Player', ply_sock, 0)
            end
          else
            player = find_player(sock)
            base_str = sock.gets # (@opts[:max_read])

            if base_str && player
              # iterate over commands and handle each one
              base_str.split("\n").each do |str|
                str = str.chomp

                begin 
                  if str =~ /^tbl_/ && player.table
                    player.table.handle_cmd(player, str)
                  elsif str =~ /^ply_/
                    player.handle_cmd(str)
                  elsif str =~ /^srv_/
                    handle_cmd(player, str)
                  else
                    raise "unknown cmd: \"#{str}\"."
                  end
                rescue 
                  p_str = "#{player.name}@#{player.addr}"
                  $stderr.puts "#{p_str}: #$!"
                end
              end
            end

            disconnect_player(player) if !sock || sock.closed?
          end
        end
      end
    end

    def main_loop
      while !@opts[:done]
        main_iteration
        dump_status
      end
    end
  end
end

if __FILE__ == $0
  server = Holdem::Server.new(nil, 12340)
  server.main_loop
end

