#!/usr/bin/env ruby

# $kcode = "utf-8"

require 'webrick'
include WEBrick
CRLFCRLF = CRLF + CRLF

require 'yaml'
require 'fileutils'
require 'pp'

PROGNAME='webcam-emu'
PIDNAME = "/var/run/#{PROGNAME}.pid"

$CNF = nil
[ '.', "/etc/#{PROGNAME}" , '/etc', ENV['HOME'] ].each do | dir |
   begin
      confpath = dir + "/#{PROGNAME}.yaml"
      $CNF = YAML::load( File.open( confpath ) )
      break
   rescue Errno::ENOENT
      next
   end
end

raise "Not found config file `#{PROGNAME}.yaml'" if $CNF.nil?

$CNF['Daemonize'] = true if ARGV[0] =~ /daemon/
   pp $CNF if $CNF['Debug']

Dir.chdir($CNF['JpegsRoot'])

$httpd = nil
at_exit { $httpd.shutdown if $httpd }

class FilesHash < Hash
   def initialize
      @h = Hash.new
   end

   def load(subpath1, subpath2, ext_pat)
      if @h.has_key?(subpath1) && @h[subpath1].has_key?(subpath2)
         return @h[subpath1][subpath2]
      end
      a = Array.new
      dir = subpath1 + '/' +  subpath2
      Dir.foreach(dir) do | elem |
         path = dir + '/' + elem
      stat = File::Stat.new(path)
      next if path !~ ext_pat
      next if not stat.file? or not stat.readable?
      a << Hash[:name => path, :size => stat.size]
      end
      raise "Could't find files in `#{dir}/'" if a.empty?
      a1 = a.sort {|a,b| a[:name]<=>b[:name]}
      @h[subpath1] = Hash[ subpath2 => a1 ]
      a1
   end
end

$Files = FilesHash.new

def parse_res_col ( res, col )

   resolution = ( res || $CNF['DefResolution'] )

   if 0 != Integer( col || $CNF['DefColor']) 
      color = 'color'
   else
      color = 'grey'
   end

   return resolution, color
end

class JpegRequest < HTTPServlet::AbstractServlet
   def do_GET(req, resp)

      resolution = req.query['resolution'] || @config['DefResolution']
      if 0 != Integer( req.query['color']  || @config['DefColor'])
         color = 'color'
      else
         color = 'grey'
      end

      begin
         flist = $Files.load(resolution, color, /\.jpe*g$/i)
      rescue Errno::ENOENT => detail
         raise HTTPStatus::PreconditionFailed.new(detail.message)
      end

      next_file = flist[Random.rand(0...(flist.length-1))]
      resp.filename =  next_file[:name]
      resp.content_type = 'image/jpeg'
      resp.header['content-length'] = next_file[:size] 

      if resp.keep_alive
         resp.header['connection'] = 'keep-alive'
      else
         resp.header['connection'] = 'close'
      end
      if req.request_method == 'HEAD'
         resp.content_length = next_file[:size]
      else
         resp.header['access-control-allow-origin'] = "*"
         resp.body = File.open( resp.filename )
      end
      raise HTTPStatus::OK
   end
end

class MjpegRequest < HTTPServlet::AbstractServlet
   def do_GET(req, resp)
      # peer_ip = req.peeraddr[3]
      # peer_port = req.peeraddr[1]

      class << resp
         attr :resolution, true
         attr :color, true
         attr :fps, true
         attr :frame_period, true
         attr :mode, true
         attr :jpegs, true

         # override http response header preparing
         def setup_header()
            super
            if @status == 200 
               @header.delete('content-length')
               @header['content-type'] = "multipart/x-mixed-replace; boundary=#{@config['MjpgHandler'][:boundary]}"
               @header['access-control-allow-origin'] = "*"
               @header['connection'] = "close"
            end
         end

         def send_body(socket)
            if @request_method == "HEAD" or not @status == 200
               super
               return
            end
            def @jpegs.next_file(mode, fps, frame_period)
               raise "Empty jpeg files array" if self.empty?
               cur = Random.rand(0...(self.length-1))
               step = 1
               step = fps if mode == 1
               while true
                  t_start_file = Time.now
                  if mode == 2 and cur == 0
                     t_start_series = t_start_file
                  end
                  yield self[cur]
                  t_end_file = Time.now
                  break if $httpd.status == :Shutdown
                  delay = t_start_file + frame_period - t_end_file
                  sleep(delay) if delay > 0
                  if mode == 2 and t_end_file.sec != t_start_series.sec
                     cur = 0
                  else
                     cur =  (cur + step) % self.length
                  end
               end
            end

            # transmit the headers
            socket.flush

            dash_boundary = "--" + @config['MjpgHandler'][:boundary]
            jpegs.next_file(@mode, @fps, @frame_period) do | f |
               entity_headers = ''
               entity_headers << CRLF << dash_boundary << CRLF
               entity_headers << "Content-Type: image/jpeg" << CRLF
               entity_headers << "Content-Length: #{f[:size]}" << CRLF
               #entity_headers << "X-Field: #{f[:size]}" << CRLF
               entity_headers << CRLF
               socket << entity_headers
               socket << File.open(f[:name]).read
               socket.flush
            end
         end # send_body_io()
      end # class

      resp.resolution = req.query['resolution'] || @config['DefResolution']
      if 0 != Integer( req.query['color'] || @config['DefColor'])
         resp.color = 'color'
      else
         resp.color = 'grey'
      end
      resp.fps = Integer(req.query['fps'] || @config['MjpgHandler'][:default_fps])
      resp.frame_period = 1.0/resp.fps

      resp.mode = Integer( req.query['mode'] || @config['MjpgHandler'][:default_mode])
      resp.mode = 0 if ( resp.mode > 3 )

      begin
         resp.jpegs = $Files.load(resp.resolution, resp.color, /\.jpe*g$/i)
      rescue Errno::ENOENT => detail
         raise HTTPStatus::PreconditionFailed.new(detail.message)
      end

      raise HTTPStatus::OK
   end
end

class AudioRequest < HTTPServlet::AbstractServlet
   def do_GET(req, resp)
      # peer_ip = req.peeraddr[3]
      # peer_port = req.peeraddr[1]

      class << resp
         attr :audios, true
         attr_reader(:httptype)

         # override http response header preparing
         def setup_header()
            super
            httptype_s = HTTPUtils::parse_query(@request_uri.query)['httptype'] || @config['DefResolution']
            case httptype_s
            when /single/i
               @httptype = :SIGNLEPART
               @header['content-type'] = 'audio/basic'
            when /multi/i
               @httptype = :MULTIPART
               @header['content-type'] = "multipart/x-mixed-replace; boundary=#{@config['MjpgHandler'][:boundary]}"
            else
               @httptype = :SIGNLEPART
               @header['content-type'] = 'audio/basic'
            end
            if not @request_method == 'HEAD' and @status == 200
               @header.delete('content-length')
            end
         end

         def send_body(socket)
            if @request_method == "HEAD" or not @status == 200
               super
               return
            end
            def @audios.next_block(block_sz = 240)
               raise "Empty array of audio files" if self.empty?
               file_nr = 0
               buf = ''
               while $httpd.status != :Shutdown
                  finfo = self[file_nr]
                  File.open(finfo[:name],'rb') do |fio|
                     while $httpd.status != :Shutdow
                        begin
                           buf << fio.readpartial(block_sz-buf.length)
                        rescue EOFError
                        end
                        break if buf.length < block_sz
                        yield buf
                        buf = ''
                        sleep(0.03)
                     end
                  end
                  file_nr =  (file_nr + 1) % self.length
               end
            end
            # transmit the headers
            socket.flush

            if @httptype == :MULTIPART
               dash_boundary_and_CRLF = "--" + @config['MjpgHandler'][:boundary] + CRLF
               socket << dash_boundary_and_CRLF
            end
            audios.next_block do | block |
               if @httptype == :MULTIPART
                  socket << "Content-Type: audio/basic" << CRLF
                  # socket << "Content-Length: #{f[:size]}" << CRLFCRLF
               end
               socket << block
               socket << CRLF << dash_boundary_and_CRLF if @httptype == :MULTIPART
               socket.flush
               break if $httpd.status == :Shutdown
            end
         end # send_body()
      end # class

      begin
         resp.audios = $Files.load('audio', 'basic', /\.ul$/i)
      rescue Errno::ENOENT => detail
         raise HTTPStatus::PreconditionFailed.new(detail.message)
      end

      raise HTTPStatus::OK
   end
end

def start_webrick(config = {})
   if config['Daemonize']
      if File.exist?(PIDNAME)
         STDERR.puts "Pidfile `#{PIDNAME}' exist. Maybe \"#{PROGNAME}\" already started?"
         exit 1
      end

      config.update(:ServerType => Daemon)
      config.update(:StopCallback => Proc.new{ FileUtils.rm_f PIDNAME })
      config.update(:Logger => Log.new('/dev/null'))

      class << Daemon
         def Daemon.start
            exit!(0) if fork
            Process::setsid
            exit!(0) if fork
            # Dir::chdir("/")
            File::umask(0)

            open(PIDNAME, 'w', 0644 ) do | pid_file_io |
               pid_file_io << Process.pid.to_s
            end

            STDIN.reopen("/dev/null")
            STDOUT.reopen("/dev/null", "w")
            STDERR.reopen("/dev/null", "w")
            yield if block_given?
         end
      end
   end # if Daemonize

   # config.update(:AcceptCallback => Proc.new{ |sock| sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)} )

   $httpd = HTTPServer.new(config)
   config['JpegHandler'][:aliases].each   { | jpeg_alias  | $httpd.mount(jpeg_alias,  JpegRequest) }
   config['MjpgHandler'][:aliases].each   { | mjpg_alias  | $httpd.mount(mjpg_alias,  MjpegRequest) }
   config['AudioHandlers'][:aliases].each { | audio_alias | $httpd.mount(audio_alias, AudioRequest) }

   yield $httpd if block_given?

   ['INT', 'TERM'].each do |signal|
      trap(signal) { $httpd.shutdown }
   end

   $httpd.start
end

start_webrick( $CNF )
