require 'thread'
require 'yaml'
require 'myth'
require 'nc'
require 'date'
require 'list'
require 'named'
require 'timeout'
require 'strsearch'

class ListWindow
  extend Forwardable
  def_delegators :window,
    *Ncurses::WINDOW.instance_methods.reject {|x| x[/^__/]}

  attr_accessor :items, :window
  def window= w
    @window = w
  end

  def initialize *args, &block
    self.window = Ncurses::WINDOW.new *args
    @print = block || proc {:print}
  end

  def item_block &block
    @items = ProcVar.new &block
  end
  def color_block &block
    @color = block
  end

  def draw offset=nil, &block
    offset ||= 0
    block ||= @print
    move 0, 0
    itm ||= 0
    l = getmaxy
    o = [[items.length - l, offset - l/2].min, 0].max
    items[o, getmaxy].each_with_index do |*args|
      use_pair @color.call(*args[0, @color.arity]) if @color
      if block
        a = block.arity
        a = args.length if a == -1
        block.call *args[0, a]
      else
        puts args.first.to_s
      end
    end
    use_pair 0
  end

  def refresh offset=nil
    draw offset
    window.refresh
  end
end

class MythClient::Recording
  def properties sel=$selected
    props = []
    props << :new if hasairdate == "1" and
      original_airdate >= Time.parse(Date.today.ctime)
    props << :playing if Time.now.between? start_time, end_time
    return props if sel.nil?
    return props + [:selected] if sel == self

    if sel.programid == programid
      props << :episode
    elsif sel.seriesid == seriesid
      props << :series
    end
    props << :concurrent if start_time < sel.end_time and
      end_time > sel.start_time
    props
  end

  def playing?
    properties.include? :playing and (Time.now - start_time).to_f / length
  end
  def length
    end_time - start_time
  end

  def flags
    p = properties
    pp = ""
    pp << "-> " if p.include? :selected
    pp << "~  " if p.include? :concurrent
    pp << "@" if p.include? :series
    pp << "*" if p.include? :episode
    pp << "N" if p.include? :new
    pp << "P" if p.include? :playing
    pp << case status
      when :recording, :later, :earlier, :watched, :max, :conflict, :override
        status.to_s.upcase[0,1]
      when :scheduled: "R"
      when :recorded: "S"
      when :repeat: "r"
      when :inactive: "x"
      when 0, "0"
        recpriority
      else "#{status}"
    end
    pp
  end

  def to_s
    time_format = "%d %H:%M"
    format = "%flags:- 5s %priority:- 2d %date:s %channel:s %title:s - %subtitle:s"
    date = start_time.strftime(time_format)
    prior = recpriority
    sub = subtitle
    sub = original_airdate.strftime("%B %d, %Y") if sub == ""
    format % [flags, prior, date, channelcallsign, title, sub]
  end
end

class Cont
  def initialize host
    host = host.to_s
    @freq = 60
    @client = MythClient.new host
    @shows = List.new
    @mut = Mutex.new
    init = ConditionVariable.new

    @mut.synchronize do
      @updater = Thread.new do
        while true
          begin
            #`mplayer /av/sounds/tap1.wav 2> /dev/null`
            shows = @client.upcoming
            expiring = @client.expiring
            free = @client.free_space
            @mut.synchronize do
              @shows.load shows
              @expiring = expiring
              @free = free
              @updated = Time.now
              init.signal if init
            end
          rescue
           `mplayer /av/sounds/dive.wav 2> /dev/null`
            @client = MythClient.new host
          end
          sleep @freq unless init
        end
      end
      init.wait @mut
      init = nil
    end
  end

  def set_mode mode
    #raise ArgumentError, "can't set mode, no selection" unless shows.current
    c = shows.current || shows.next
    mode = :all unless c
    @mut.synchronize do
      case mode
        when :all: @shows.select!
        when :channel: @shows.select! {|s| s.channelid == c.channelid}
        when :series: @shows.select! {|s| s.seriesid == c.seriesid}
        when :episode: @shows.select! {|s| s.programid == c.programid}
        when :category
          @shows.select! do |s|
            !(s.category.split(",") & c.category.split(",")).empty?
          end
        when :concurrent
          @shows.select! do |s|
            s == c or s.properties(c).include? :concurrent
          end
        when :new: @shows.select! {|s| s.properties(c).include? :new}
        else raise ArgumentError, "no such mode: #{mode}"
      end
    end
    @shows.select @shows.selary.rindex(c)
    mode
  end

  def method_missing sym, *args
    if [:all, :channel, :series, :episode, :category, :concurrent, :new].include? sym
      set_mode sym, *args
    else
      super
    end
  end

  def shows
    @mut.synchronize {@shows}
  end

  def expiring
    @mut.synchronize {@expiring}
  end

  def updated
    @mut.synchronize {@updated}
  end

  def free
    @mut.synchronize {@free}
  end

  attr_reader :client

  def search string
    shows.select! do |s|
      "#{s.title} #{s.subtitle} #{s.description}".search string
    end
  end

  extend Forwardable
  #def_delegators :@client, *MythClient.instance_methods.reject {|x| x[/^__/]}
  def_delegators :shows, :next, :prev, :select, :current, :[], :each, :select!, :first, :last, :position, :selected
  include Enumerable

  def inspect
    "\n" + map {|x| x.to_s}.join("\n")
  end

  def die
    @updater.terminate
    @updater.join
  end
end

if __FILE__ == $0
  host = (ARGV[0] || "bombadil")
  $data = Cont.new host
  $selected = ProcVar.new { $data.current }
  $space = $data.client.buffer_space * 5

  colors = YAML.load File.read("colors.yaml")

  Ncurses.ncinit do
    initscr
    noecho
    halfdelay 10
    start_color

    h, w = Ncurses.LINES, Ncurses.COLS
    expirenum = 10

    keys = colors.keys
    keys.unshift keys.delete(:normal) # make sure normal is first
    keys.compact!
    keys.each_with_index do |k,i|
      add_pair colors[k][0], colors[k][1], k
      if colors[k].length > 2
        add_pair colors[k][2], colors[k][3], k+"bg"
      end
    end
    # these are the ones shown in the legend
    keys = %w|normal next_expire recording later scheduled earlier
    watched recorded max repeat inactive conflict|.map {|x| x.to_sym}

    # update loop
    #update = Thread.new do
      begin
        Signal.trap "SIGINT" do exit end
        delay = 1
        upcoming = ListWindow.new Ncurses.LINES - 10, Ncurses.COLS, 0, 0 do |s|
          str = s.to_s.ljust upcoming.getmaxx
          if p = s.playing?
            stat = s.status.to_s
            upcoming.puts_bicolor str, stat, "#{stat}bg", p
          else
            upcoming.puts str
          end
        end
        upcoming.color_block {|s| s.status.to_s}
        upcoming.item_block {$data.shows}
        expiring = ListWindow.new 9, Ncurses.COLS, Ncurses.LINES-9, 0 do |s|
          expiring.puts s.to_s.ljust(expiring.getmaxx)
        end
        expiring.color_block {|s| s == expiring.items.first ? "next_expire" : "normal"}
        expiring.item_block {$data.expiring}
        while true
          stdscr.move 0, 0
          #$data[0, Ncurses.LINES].each do |s|
          #  Ncurses.use_pair s.status.to_s
          #  str = s.to_s.ljust(Ncurses.COLS)
          #  if p = s.playing?
          #    stat = s.status.to_s
          #    stdscr.puts_bicolor str, "#{stat}", "#{stat}bg", p
          #  else
          #    stdscr.puts str.ljust(Ncurses.COLS)
          #  end
          #end
          #upcoming.draw $data.shows.position
          #c = timeout(delay, RuntimeError) {Ncurses.getch} rescue nil
          c = Ncurses.getch
          case c
            when ?q: break
            when 27: $data.select nil
            when ?j: $data.next
            when ?k: $data.prev
            when ?s: $data.series and upcoming.clear
            when ?e: $data.episode and upcoming.clear
            when ?c: $data.channel and upcoming.clear
            when ?x: $data.concurrent and upcoming.clear
            when ?z: $data.category and upcoming.clear
            when ?a: $data.all and upcoming.clear
            when ?n: $data.new and upcoming.clear
            when ?/
              str = ""
              stdscr.getstr str
              $data.search str
              upcoming.clear
          end
          upcoming.refresh $data.position
          expiring.refresh
          stdscr.move Ncurses.LINES - 10, 0
          p = ($space - $data.free) / $space
          str = ([(" " * ((Ncurses.COLS - 4) / 5))] * 5).join "#"
          stdscr.puts_bicolor str, "separator", "separatorbg", p
          stdscr.move Ncurses.LINES - 1, Ncurses.COLS - 1
          refresh
        end
      #rescue Exception => e
        #raise
        #$main.kill
      end
    #end.join
    # main loop
#    $main = Thread.new do
#      c = Ncurses.getch
#      case c
#        when ?q: break
#        when ?j: $data.next
#        when ?k: $data.prev
#        when ?s: $data.series and Ncurses.clear
#        when ?e: $data.episode and Ncurses.clear
#        when ?c: $data.channel and Ncurses.clear
#        when ?a: $data.all and Ncurses.clear
#      end
#    end.join

    $data.die

    exit

    $windows = {}

    $windows[:upcoming] = ListWindow.new h - expirenum - 1, w, 0, 0 do |i|
      t = Time.now
      start = i.start_time.strftime(time_format)
      str = show_format % [t, i.channelcallsign, i.title, i.subtitle]
      if t >= i.start_time and t <= i.end_time
        percent = (t - i.start_time).to_f / (i.end_time - i.start_time)
        i.puts_bicolor str, i.status, i.status+"bg", percent
      else
        use_pair i.status
        i.puts str
      end
    end
    expiring = ListWindow.new expirenum, w, h - expirenum - 1, 0 do |s, i|
      if i == 0
        use_pair "next_expire"
      else
        use_pair "normal"
      end
      puts "#{i.title} -- #{i.subtitle}"
    end
    clear
    format = "%H:%M:%S"
    while true
      expire = [Ncurses.LINES/3, 10].min
      stdscr.move 0, 0
      $mut.synchronize do
        $shows[0,Ncurses.LINES - expire - 1].each do |s|
          use_pair s.status.to_s
          p = properties s
          pp = ""
          pp << "N" if p.include? :new
          pp << "P" if p.include? :playing
          pp << case s.status
            when :recording: "R"
            when :later: "L"
            when :scheduled: "R"
            when :earlier: "E"
            when :watched: "W"
            when :recorded: "S"
            when :max: "M"
            when :repeat: "r"
            when :conflict: "C"
            else "#{s.status}"
          end
          str = show_format % [pp, s.start_time.strftime(time_format), s.channelcallsign, s.title, s.subtitle]
          #str = "#{s.start_time.strftime format} on #{s.channelcallsign}: #{s.title} - #{s.subtitle}".ljust Ncurses.COLS
          t = Time.now
          p = if t > s.start_time and t < s.end_time
            (Time.now - s.start_time).to_f / (s.end_time - s.start_time)
          else
            0
          end
          stdscr.puts_bicolor str, s.status.to_s, "#{s.status}bg", p
        end
        half = $expiring.select do |s|
          t = s.end_time - s.start_time
          (t - 60*30).abs < 10
        end.map {|x| x.filesizehigh.to_i}.reject {|x| x.nil? or x <= 0}
        bigsize = half.max / 1024.0 ** 3 # gigs
        ticks = 10
        bigsize *= ticks
        free = $client.free_space - $client.buffer_space
        percent = [[0, 1 - free / bigsize].max, 1].min
        str = "updated: #{$update.strftime(format)} Free Space: #{"%0.3f" % free} #{$shows.first.filesizehigh} Expiring...".ljust(Ncurses.COLS)
        stdscr.puts_bicolor str, "separator", "separatorbg", percent
        ticksize = Ncurses.COLS / ticks
        y, x = stdscr.getyx
        y -= 1
        Ncurses.use_pair "separator"
        ticks.times do |i|
          next if i.zero?
          pos = ticksize * (i)
          stdscr.print pos, y, str[pos,1]
        end
        stdscr.move y+1, x
        use_pair "next_expire"
        $expiring[0,expire].each do |e|
          stdscr.puts "#{e.title} - #{e.subtitle}".ljust(Ncurses.COLS)
          use_pair "normal"
        end

        y = Ncurses.LINES - expire - keys.length - 2
        keys.each_with_index do |k,i|
          stdscr.move y+i, Ncurses.COLS - k.to_s.length - 2
          use_pair k.to_s
          stdscr.print " #{k}"
        end
      end
      refresh
      sleep 1
    end
    gets
  end
  exit
end

if __FILE__ == $0
  $selected = nil

  host = (ARGV[0] || "bombadil")
  $client = MythClient.new host
  $mut = Mutex.new
  init = ConditionVariable.new
  $mut.synchronize do
    Thread.new do
      while true
        begin
          tmp = $client.upcoming
          tmp2 = $client.expiring
          $mut.synchronize do
            $shows = tmp
            $expiring = tmp2
            $update = Time.now
            init.signal if init
          end
        rescue
          $client = MythClient.new host
        end
        sleep Time.now.sec
      end
    end
    init.wait $mut
    init = nil
  end

  colors = YAML.load File.read("colors.yaml")

  Ncurses.ncinit do
    initscr
    noecho
    start_color

    h, w = Ncurses.LINES, Ncurses.COLS
    expirenum = 10
    time_format = "%d %H:%M"
    show_format = "%s %s on %s: %s - %s"
    upcoming = ListWindow.new h - expirenum - 1, w, 0, 0 do |i|
      t = Time.now
      start = i.start_time.strftime(time_format)
      str = show_format % [t, i.channelcallsign, i.title, i.subtitle]
      if t >= i.start_time and t <= i.end_time
        percent = (t - i.start_time).to_f / (i.end_time - i.start_time)
        puts_bicolor str, i.status, i.status+"bg", percent
      else
        use_pair i.status
        puts str
      end
    end
    expiring = ListWindow.new expirenum, w, h - expirenum - 1, 0 do |s, i|
      if i == 0
        use_pair "next_expire"
      else
        use_pair "normal"
      end
      puts "#{i.title} -- #{i.subtitle}"
    end
    keys = colors.keys
    keys.unshift keys.delete(:normal) # make sure normal is first
    keys.compact!
    keys.each_with_index do |k,i|
      add_pair colors[k][0], colors[k][1], k
      if colors[k].length > 2
        add_pair colors[k][2], colors[k][3], k+"bg"
      end
    end
    keys = [:normal, :next_expire, :recording, :later, :scheduled, :earlier, :watched, :recorded, :max, :repeat, :conflict]
    clear
    format = "%H:%M:%S"
    while true
      expire = [Ncurses.LINES/3, 10].min
      stdscr.move 0, 0
      $mut.synchronize do
        $shows[0,Ncurses.LINES - expire - 1].each do |s|
          use_pair s.status.to_s
          p = properties s
          pp = ""
          pp << "N" if p.include? :new
          pp << "P" if p.include? :playing
          pp << case s.status
            when :recording: "R"
            when :later: "L"
            when :scheduled: "R"
            when :earlier: "E"
            when :watched: "W"
            when :recorded: "S"
            when :max: "M"
            when :repeat: "r"
            when :conflict: "C"
            else "#{s.status}"
          end
          str = show_format % [pp, s.start_time.strftime(time_format), s.channelcallsign, s.title, s.subtitle]
          #str = "#{s.start_time.strftime format} on #{s.channelcallsign}: #{s.title} - #{s.subtitle}".ljust Ncurses.COLS
          t = Time.now
          p = if t > s.start_time and t < s.end_time
            (Time.now - s.start_time).to_f / (s.end_time - s.start_time)
          else
            0
          end
          stdscr.puts_bicolor str, s.status.to_s, "#{s.status}bg", p
        end
        half = $expiring.select do |s|
          t = s.end_time - s.start_time
          (t - 60*30).abs < 10
        end.map {|x| x.filesizehigh.to_i}.reject {|x| x.nil? or x <= 0}
        bigsize = half.max / 1024.0 ** 3 # gigs
        ticks = 10
        bigsize *= ticks
        free = $client.free_space - $client.buffer_space
        percent = [[0, 1 - free / bigsize].max, 1].min
        str = "updated: #{$update.strftime(format)} Free Space: #{"%0.3f" % free} #{$shows.first.filesizehigh} Expiring...".ljust(Ncurses.COLS)
        stdscr.puts_bicolor str, "separator", "separatorbg", percent
        ticksize = Ncurses.COLS / ticks
        y, x = stdscr.getyx
        y -= 1
        Ncurses.use_pair "separator"
        ticks.times do |i|
          next if i.zero?
          pos = ticksize * (i)
          stdscr.print pos, y, str[pos,1]
        end
        stdscr.move y+1, x
        use_pair "next_expire"
        $expiring[0,expire].each do |e|
          stdscr.puts "#{e.title} - #{e.subtitle}".ljust(Ncurses.COLS)
          use_pair "normal"
        end

        y = Ncurses.LINES - expire - keys.length - 2
        keys.each_with_index do |k,i|
          stdscr.move y+i, Ncurses.COLS - k.to_s.length - 2
          use_pair k.to_s
          stdscr.print " #{k}"
        end
      end
      refresh
      sleep 1
    end
    gets
  end
end
