require 'time'
require 'socket'
require 'forwardable'
require 'readline'
require 'thread'
require 'things'

class MythSock < TCPSocket
  class Packet
    def initialize *payload
      @payload = payload.join "[]:[]"
      @size = @payload.length.to_s
    end

    def payload
      @payload.split "[]:[]"
    end

    def to_s
      "%-8d%s" % [@size, @payload]
    end

    def self.prepare *payload
      new(*payload).to_s
    end
  end

  def initialize host, port=6543
    super
    @write, @read = Mutex.new, Mutex.new
  end

  def send *p
    @write.synchronize { print Packet.new(p) }
  end
  def recv wait=0 # 0 means wait forever
    @read.synchronize do
      listen unless @queue
      unless wait.zero?
        sleep wait if @queue.empty?
        return nil if @queue.empty?
      end
      @queue.shift
    end
  end

  def listen
    return if @th
    @queue ||= Queue.new
    @th = Thread.new do
      begin
        while true
          size = ""
          size << read(8 - size.length) while size.length < 8
          size = size.to_i
          packet = ""
          packet << read(size - packet.length) while packet.length < size
          packet = Packet.new packet
          @queue << packet
        end
      ensure @th = nil
      end
    end
  end
  private :listen
end

class MythClient
  VERSION = (ENV['MYTHVERSION'] || 31).to_i
  MYTH_PORT = (ENV['MYTHPORT'] || 6543).to_i

  fields = [
    :title,
    :subtitle,
    :description,
    :category,
    :channelid,
    :channelnumber,
    :channelcallsign,
    :channelname,
    :filename,
    :filesizelow,
    :filesizehigh,
    :starttime,
    :endtime,
    :duplicate,
    :shareable,
    :findid,
    :hostname,
    :sourceid,
    :cardid,
    :inputid,
    :recpriority,
    :recstatus,
    :recordid,
    :rectype,
    :dupin,
    :dupmethod,
    :recordingstart,
    :recordingend,
    :repeat,
    :programflags,
    :recordinggroup,
    :chancommfree,
    :channeloutputfilters,
    :seriesid,
    :programid,
    :lastmodified,
    :stars,
    :originalairdate,
    :hasairdate,
    :playgroup,
    :recpriority2,
    :parentid,
    :storagegroup
  ]
  fields.delete :storagegroup if VERSION < 32
  fields.delete :parentid if VERSION < 31
  fields += [:unknown1, :unknown2, :unknown3] if VERSION >= 40

  Recording = Struct.new *fields
  class Recording
    # statuses
    RECORDING = -2
    SCHEDULED = -1
    OVERRIDE = 1
    WATCHED = 2
    RECORDED = 3
    EARLIER = 4
    MAX = 5
    INACTIVE = 6
    CONFLICT = 7
    LATER = 8
    REPEAT = 9
    def status
      case recstatus.to_i
        when RECORDING
          :recording # currently recording
        when SCHEDULED
          :scheduled # will record
        when WATCHED
          :watched # previously recorded
        when RECORDED
          :recorded # currently recorded
        when EARLIER
          :earlier # record earlier showing
        when MAX
          :max # maximum episodes stored
        when INACTIVE
          :inactive
        when CONFLICT
          :conflict # has a conflict
        when LATER
          :later # record later showing
        when REPEAT
          :repeat # repeat episode (not spankin' new)
        when OVERRIDE
          :override
        else
          recstatus
      end
    end

    def start_time
      Time.at(0) + starttime.to_i
    end
    def end_time
      Time.at(0) + endtime.to_i
    end
    def original_airdate
      Time.at(0) + originalairdate.to_i
    end
  end

  extend Forwardable
  def_delegators :@sock, :send, :recv
  class VersionError < Exception
    def initialize versions
      @vers = versions # [client, server]
      super
    end

    def to_s
      "Version mismatch: client=#{@vers[0]}, server=#{@vers[1]}"
    end
  end

  def initialize host, opts={}, &block
    opts[:port] ||= MYTH_PORT
    opts[:mode] ||= :Monitor
    @sem = Mutex.new
    @sock = MythSock.new host, opts[:port]
    send "MYTH_PROTO_VERSION #{VERSION}"
    resp = recv.payload
    raise VersionError, [VERSION, resp[1]] if resp[0] != "ACCEPT"
    mode opts[:mode], &block
  end

  def mode m
    events = block_given? ? 1 : 0
    raise ArgumentError unless
      [:Playback, :Monitor, :FileTransfer].include? m.to_sym
    p = nil
    @sem.synchronize do
      send "ANN %s %s %s" % [m, Socket.gethostname, events]
      p = recv
      #raise p unless p.payload.first == "OK"
    end
    if block_given?
      yield p
      Thread.new do
        pp = nil
        while true
          @sem.synchronize {pp = recv(1)}
          yield pp if pp
          sleep 1
        end
      end
    end
    p
  end

  def query *args
    @sem.synchronize do
      send args
      r = recv.payload
    end
  end
  def objects ary
    h = ary.shift.to_i
    raise RuntimeError, "missing fields!" unless (ary.length%h).zero?
    w = ary.length/h
    raise ArgumentError, "fields don't match" unless
      w == Recording.members.length
    h.things {|i| Recording.new *ary[i*w,w]}
  end

  def upcoming
    r = query "QUERY_GETALLPENDING"
    conflicts = !r.shift.to_i.zero?
    objects r
  end
  def schedules
    r = query "QUERY_GETALLSCHEDULED"
    objects r
  end
  def recordings
    r = query "QUERY_RECORDINGS Play"
    objects r
  end
  def expiring
    r = query "QUERY_GETEXPIRING"
    objects r
  end

  def recording? tuner=1
    r = query "QUERY_RECORDER #{tuner}", "IS_RECORDING"
    !r.first.to_i.zero?
  end

  def recording tuner=1
    Recording.new *query("QUERY_RECORDER #{tuner}", "GET_RECORDING")
  end

  def buffer_space
    tmp = query "QUERY_SETTING mythtv AutoExpireExtraSpace"
    tmp.first.to_i
  end
  def free_space
    tmp = query "QUERY_FREE_SPACE"
    (tmp[1].to_f - tmp[3].to_f) / 1024 ** 2 # in gigs
  end

  def close
    send "DONE"
    @sock.close
  end
end

if __FILE__ == $0
  serv = (ARGV[0] or "bombadil")
  c = MythClient.new(serv)
  c.mode("Monitor") {|p| puts "[%s]" % p.payload.join(", ")}
  while s = STDIN.gets
    c.send s.chomp
  end
  c.close
end
