require 'rubygems'
require 'open3'
require 'pathname'
require 'timeout'
require 'unicode'

module TranscodingMachine
  module Server
    class MediaFileAttributes < Hash
      ASPECT_RATIO_2_35_BY_1 = 2.35
      ASPECT_RATIO_16_BY_9 = 16.0 / 9.0
      ASPECT_RATIO_4_BY_3 = 4.0 / 3.0
      ASPECT_RATIO_NAMES = {ASPECT_RATIO_2_35_BY_1 => '2.35/1', ASPECT_RATIO_16_BY_9 => '16/9', ASPECT_RATIO_4_BY_3 => '4/3'}
      ASPECT_RATIO_VALUES = ASPECT_RATIO_NAMES.invert

      CODECS = {'ffmp3' => :mp3, 'mp3' => :mp3, 'faad' => :aac, 'ffh264' => :h264, 'h264' => :h264, 'ffvp6f' => :flash_video}

      TRACK_FIELD_TYPES = {
        :codec => :codec,
        :width => :integer,
        :height => :integer,
        :format => :string,
        :aspect => :float,
        :id => :integer,
        :bitrate => :integer,
        :fps => :float,
        :file_name => :string,
        :length => :float,
        :demuxer => :string,
        :rate => :integer,
        :channels => :integer
      }

      FIELD_TYPES = {
        :audio => :boolean,
        :audio_format => :string,
        :audio_rate => :integer,
        :audio_bitrate => :integer,
        :audio_codec => :codec,
        :audio_channels => :integer,
        :video => :boolean,
        :video_format => :string,
        :video_codec => :codec,
        :width => :integer,
        :height => :integer,
        :aspect_ratio => :float,
        :video_fps => :float,
        :video_bitrate => :integer,
        :ipod_uuid => :boolean,
        :bitrate => :integer,
        :length => :float,
        :file_name => :string,
        :file_extension => :string,
        :demuxer => :string,
        :poster_time => :float
      }

      def initialize(media_file_path)
        super()
        ffmpeg = FfmpegIntegrator.new(media_file_path)
        mplayer = MplayerIntegrator.new(media_file_path)

        puts "FFMPEG:\n#{ffmpeg.tracks.inspect}"

        puts "MPLAYER:\n#{mplayer.tracks.inspect}"

        store(:ipod_uuid, false)

        merge!(get_video_info(get_video_track(ffmpeg.tracks), get_video_track(mplayer.tracks)))
        merge!(get_audio_info(get_audio_track(ffmpeg.tracks), get_audio_track(mplayer.tracks)))
        merge!(get_container_info(ffmpeg.tracks[:container], mplayer.tracks[:container]))

        derive_values

        if video?
          atomic_parsley = AtomicParsleyIntegrator.new(media_file_path)
          puts "ATOMIC_PARSLEY:\n#{atomic_parsley.tracks.inspect}"
          store(:ipod_uuid, atomic_parsley.tracks[:container][:ipod_uuid])

          exiftool = ExifToolIntegrator.new(media_file_path)
          puts "EXIFTOOL:"
          puts exiftool.inspect
          store(:poster_time, exiftool.poster_time)
          if exiftool.aspect_ratio and video_aspect != exiftool.aspect_ratio
            store(:height, (width / exiftool.aspect_ratio).to_i)
            store(:video_aspect, exiftool.aspect_ratio)
          elsif exiftool.width && exiftool.height
            store(:width, exiftool.width)
            store(:height, exiftool.height)
            derive_values
          end
          fix_dimensions
        end

        delete_if {|key, value| value.nil?}
      end

      def video?
        video
      end

      def audio?
        audio
      end

      def thumbnail_file
        return @thumbnail_file if @thumbnail_file

        return nil unless video? and height and width

        time = poster_time
        if time.nil? or time == 0 or time > 30
          time = length / 10.0
          time = 10 if time > 10
        end

        @thumbnail_file = FfmpegIntegrator.create_thumbnail(file_name, width, height, time)
      end

      def self.parse_values(values)
        values.values.each do |track_values|
          track_values.each do |key, value|
            case TRACK_FIELD_TYPES[key]
            when :integer
              track_values[key] = value.to_i
            when :float
              track_values[key] = value.to_f
            when :codec
              track_values[key] = CODECS[value] || value
            end
          end
        end

        values
      end

      private

      def get_video_track(tracks)
        get_track_by_type(tracks, :video)
      end

      def get_audio_track(tracks)
        get_track_by_type(tracks, :audio)
      end

      def get_track_by_type(tracks, type)
        return nil if tracks.nil?
        tracks.values.each do |track|
          return track if track[:type] == type
        end
        nil
      end

      def has_real_video_track?(ffmpeg_video_track, mplayer_video_track)
        return false if ffmpeg_video_track.nil? && mplayer_video_track.nil?

        unless ffmpeg_video_track.nil?
          return false if ffmpeg_video_track[:codec] == 'png'
          return false if ffmpeg_video_track[:width] == 1
          return false if ffmpeg_video_track[:height] == 1
          return false if ffmpeg_video_track[:fps] && ffmpeg_video_track[:fps] > 1000
        end

        unless mplayer_video_track.nil?
          return false if mplayer_video_track[:format] == 'jpeg'
          return false if mplayer_video_track[:fps] == 0
        end

        ffmpeg_video_track ||= {}
        mplayer_video_track ||= {}

        return false if (ffmpeg_video_track[:codec].nil? && mplayer_video_track[:codec].nil?)
        return false if (ffmpeg_video_track[:width].nil? && mplayer_video_track[:width].nil?)
        return false if (ffmpeg_video_track[:height].nil? && mplayer_video_track[:height].nil?)

        return true
      end

      def has_real_audio_track?(ffmpeg_audio_track, mplayer_audio_track)
        unless ffmpeg_audio_track.nil?
          return true if ffmpeg_audio_track[:codec]
          return true if ffmpeg_audio_track[:format]
        end
        unless mplayer_audio_track.nil?
          return true if mplayer_audio_track[:codec]
          return true if mplayer_audio_track[:format]
        end
        return false
      end

      def get_video_info(ffmpeg_track, mplayer_track)
        return {:video => false} unless has_real_video_track?(ffmpeg_track, mplayer_track)

        ffmpeg_track ||= {}
        mplayer_track ||= {}

        output = {:video => true}

        output[:video_format] = ffmpeg_track[:format] || mplayer_track[:format]
        output[:width] = ffmpeg_track[:width] || mplayer_track[:width]
        output[:height] = ffmpeg_track[:height] || mplayer_track[:height]
        output[:video_aspect] = ffmpeg_track[:aspect] || mplayer_track[:aspect]
        output[:video_fps] = ffmpeg_track[:fps] || mplayer_track[:fps]
        output[:video_bitrate] = ffmpeg_track[:bitrate] || mplayer_track[:bitrate]

        if ffmpeg_track[:codec].class == String && mplayer_track[:codec].class == Symbol
          output[:video_codec] = mplayer_track[:codec]
        else
          output[:video_codec] = ffmpeg_track[:codec] || mplayer_track[:codec]
        end

        output
      end

      def get_audio_info(ffmpeg_track, mplayer_track)
        return {:audio => false} unless has_real_audio_track?(ffmpeg_track, mplayer_track)

        ffmpeg_track ||= {}
        mplayer_track ||= {}

        output = {:audio => true}

        output[:audio_format] = ffmpeg_track[:format] || mplayer_track[:format]
        output[:audio_rate] = ffmpeg_track[:rate] || mplayer_track[:rate]
        output[:audio_bitrate] = ffmpeg_track[:bitrate] || mplayer_track[:bitrate]
        output[:audio_channels] = ffmpeg_track[:channels] || mplayer_track[:channels]

        if ffmpeg_track[:codec].class == String && mplayer_track[:codec].class == Symbol
          output[:audio_codec] = mplayer_track[:codec]
        else
          output[:audio_codec] = ffmpeg_track[:codec] || mplayer_track[:codec]
        end

        output
      end

      def get_container_info(ffmpeg_track, mplayer_track)
        output = Hash.new

        high_prio = ffmpeg_track || {}
        low_prio = mplayer_track || {}

        output[:bitrate] = high_prio[:bitrate] || low_prio[:bitrate]
        output[:length] = high_prio[:length] || low_prio[:length]
        output[:file_name] = high_prio[:file_name] || low_prio[:file_name]
        output[:demuxer] = high_prio[:demuxer] || low_prio[:demuxer]
        output
      end

      def video_aspect_ratio
        return nil unless video?

        ratio = width.to_f / height.to_f

        return nil if ratio.nan?

        diffs = ASPECT_RATIO_NAMES.keys.map {|ar| [(ar - ratio).abs, ar]}

        diffs.sort! {|x, y| x[0] <=> y[0]}

        diffs.first[1]
      end

      def derive_values
        store(:video_aspect, video_aspect_ratio) if (video? && has_key?(:width) && has_key?(:height))

        if file_name && (m = file_name.match(/.+\.(\w+)/))
          store(:file_extension, m[1])
        end
      end

      def fix_dimensions
        [:width, :height].each do |key|
          if has_key?(key) and (fetch(key) % 2 == 1)
            store(key, fetch(key) - 1)
          end
        end
      end

      # Intercept calls
      def method_missing(method_name, *args)
        if (FIELD_TYPES[method_name])
          self[method_name]
        else
          super
        end
      end
    end

    class FfmpegIntegrator
      attr_accessor :tracks
      BINARY = ["ffmpeg"]
      OPTIONS = ["-i"]
      TIMEOUT = 60

      def initialize(file_path)
        commandline = []
        commandline += BINARY
        commandline += OPTIONS
        commandline += [file_path]
        puts "trying to run: #{commandline.join(' ')}"
        result = begin
          timeout(TIMEOUT) do
            Open3.popen3(*commandline) do |i, o, e|
              [o.read, e.read]
            end
          end
        rescue Timeout::Error => e
          puts "Timeout reached when inspecting #{file_path} with ffmpeg"
          raise e
        end

        result = result.join("\n")

        ffmpeg_values = Hash.new

        start_index = result.index("Input #0, ")
        @tracks = {}

        unless start_index.nil?
          result = result[start_index..-1]

          result.split(/\n/).each do |line|
            line.strip!
            if match = line.match(/^Duration: ((\d\d):(\d\d):(\d\d(.\d)?)), .*, bitrate: (\d+) kb\/s/)
              puts "found duration #{line}"
              ffmpeg_values[:container] = {:type => :container}
              if match[1]
                hours = match[2] ? match[2].to_i : 0
                minutes = match[3] ? match[3].to_i : 0
                seconds = match[4] ? match[4].to_f : 0
                ffmpeg_values[:container][:length] = (hours * 60 * 60) + (minutes * 60) + seconds
              end
              if match[6]
                ffmpeg_values[:container][:bitrate] = match[6].to_i * 1000
              end
            elsif match = line.match(/^Stream #0.(\d).*: Video: ([^,]*)(, [^,]*)?(, (\d+)x(\d+))(, (\d+.\d+) fps)?/)
              puts "found video #{line}"
              track_info = ffmpeg_values["track_#{match[1]}".to_sym] = {:type => :video}
              if match[2]
                track_info[:codec] = match[2]
              end
              if match[5]
                track_info[:width] = match[5]
              end
              if match[6]
                track_info[:height] = match[6]
              end
              if match[8]
                track_info[:fps] = match[8]
              end        
            elsif match = line.match(/^Stream #0.(\d).*: Audio: ([^,]*)(, (\d+) Hz)?(, (mono|stereo))?(, (\d+) kb\/s)?/)
              puts "found audio #{line}"
              track_info = ffmpeg_values["track_#{match[1]}".to_sym] = {:type => :audio}
              if match[2]
                track_info[:codec] = match[2]
              end
              if match[4]
                track_info[:rate] = match[4]
              end
              if match[6]
                if match[6] == 'stereo'
                  track_info[:channels] = 2
                elsif match[6] == 'mono'
                  track_info[:channels] = 1
                end
              end
              if match[8]
                track_info[:bitrate] = match[8].to_i * 1000
              end
            elsif match = line.match(/^Stream #0.(\d).*: Data: (.*)/)
              puts "found data #{line}"
              track_info = ffmpeg_values["track_#{match[1]}".to_sym] = {:type => :data}
            else
              puts "found other #{line}"
            end
          end

          @tracks = MediaFileAttributes.parse_values(ffmpeg_values)
        end
      end

      def self.create_thumbnail(file_path, width, height, time)
        thumbnail_file_path = "#{file_path}.jpg"
        commandline = []
        commandline += BINARY
        commandline << '-ss'
        commandline << time.to_s
        commandline << '-i'
        commandline << file_path
        commandline += ['-f', 'mjpeg', '-deinterlace', '-vframes', '1', '-an', '-y', '-s']
        commandline << "#{width}x#{height}"
        commandline << thumbnail_file_path

        puts "trying to run: #{commandline.join(' ')}"
        result = begin
          timeout(60) do
            Open3.popen3(*commandline) do |i, o, e|
              [o.read, e.read]
            end
          end
        rescue Timeout::Error => e
          puts "Timeout reached when inspecting #{file_path} with ffmpeg"
          raise e
        end
        thumbnail_file = File.new(thumbnail_file_path)
        if thumbnail_file.stat.size == 0
          FileUtils.rm_f(thumbnail_file.path)
          throw result.join
        end

        return thumbnail_file
      end
    end

    class MplayerIntegrator
      attr_accessor :tracks
      BINARY = ["mplayer"]
      OPTIONS = ["-identify", "-vo", "null", "-ao", "null", "-frames", "0", "-really-quiet", "-msgcharset", "utf8"]
      TIMEOUT = 60

      MPLAYER_TRACK_FIELD_MAP = {
        'CODEC' => :codec,
        'WIDTH' => :width,
        'HEIGHT' => :height,
        'FORMAT' => :format,
        'ASPECT' => :aspect,
        'ID' => :id,
        'BITRATE' => :bitrate,
        'FPS' => :fps,
        'FILENAME' => :file_name,
        'LENGTH' => :length,
        'DEMUXER' => :demuxer,
        'RATE' => :rate,
        'NCH' => :channels
      }

      def initialize(file_path)
        commandline = []
        commandline += BINARY
        commandline += OPTIONS
        commandline += [file_path]
        puts "trying to run: #{commandline.join(' ')}"
        result = begin
          timeout(TIMEOUT) do
            Open3.popen3(*commandline) do |i, o, e|
              [o.read, e.read]
            end
          end
        rescue Timeout::Error => e
          puts "Timeout reached when inspecting #{file_path} with mplayer"
          raise e
        end

        raise "mplayer error when inspecting #{file_path}: #{result.last}" if result.first.empty? && !result.last.empty?

        mplayer_values = {:container => {:type => :container}}

        match = result.first.match(/.*ID_AUDIO_ID=(\d).*/)
        audio_track = mplayer_values["track_#{match[1]}".to_sym] = {:type => :audio} unless match.nil?

        match = result.first.match(/.*ID_VIDEO_ID=(\d).*/)
        video_track = mplayer_values["track_#{match[1]}".to_sym] = {:type => :video} unless match.nil?

        result.first.split(/\n/).each do |line|
          #puts line
          if (match = line.match(/ID_([^_]+)(_([^=].*))?=(.*)/))
            if match[3]
              key = MPLAYER_TRACK_FIELD_MAP[match[3]] || match[3]
            else
              key = MPLAYER_TRACK_FIELD_MAP[match[1]] || match[1]
            end
            case match[1]
            when 'VIDEO'
              video_track[key] = match[4]
            when 'AUDIO'
              audio_track[key] = match[4]
            else
              mplayer_values[:container][key] = match[4]
            end
          end
        end

        @tracks = MediaFileAttributes.parse_values(mplayer_values)
      end
    end

    class AtomicParsleyIntegrator
      attr_reader :values, :tracks
      BINARY = ["AtomicParsley"]
      OPTIONS = ['-T', '1']
      TIMEOUT = 60

      def initialize(file_path)
        commandline = []
        commandline += BINARY
        commandline += [file_path]
        commandline += OPTIONS
        puts "trying to run: #{commandline.join(' ')}"
        result = begin
          timeout(TIMEOUT) do
            Open3.popen3(*commandline) do |i, o, e|
              o.read
            end
          end
        rescue Timeout::Error => e
          puts "Timeout reached when inspecting #{file_path} with AtomicParsley"
          raise e
        end

        @tracks = {:container => {:type => :container, :ipod_uuid => false}}

        atomic_parsley_values = Hash.new

        if result =~ /Atom uuid=6b6840f2-5f24-4fc5-ba39-a51bcf0323f3/
          @tracks[:container][:ipod_uuid] = true
          atomic_parsley_values[:ipod_uuid] = true
        end
        @values = atomic_parsley_values
      end
    end

    class ExifToolIntegrator
      attr_accessor :aspect_ratio, :poster_time, :width, :height
      BINARY = ["exiftool"]
      OPTIONS = ["-q", "-q", "-s", "-t"]
      TIMEOUT = 60

      def initialize(file_path)
        commandline = []
        commandline += BINARY
        commandline += OPTIONS
        commandline += [file_path]
        puts "trying to run: #{commandline.join(' ')}"
        result = begin
          timeout(TIMEOUT) do
            Open3.popen3(*commandline) do |i, o, e|
              o.read
            end
          end
        rescue Timeout::Error => e
          puts "Timeout reached when inspecting #{file_path} with ExifTool"
          raise e
        end

        @values = Hash.new

        result.split(/\n/).each do |line|
          line.strip!
          if match = line.match(/([^\t]+)\t(.+)/)
            @values[match[1].underscore] = match[2]
          end
        end

        unless @values['aspect_ratio'].blank?
          aspect_ratio_match = @values['aspect_ratio'].match(/\d+:\d+/)
          aspect_ratio_match = aspect_ratio_match[0] if aspect_ratio_match
          case aspect_ratio_match
          when "16:9", "16/9"
            @aspect_ratio = MediaFileAttributes::ASPECT_RATIO_16_BY_9
          when "4:3", "4/3"
            @aspect_ratio = MediaFileAttributes::ASPECT_RATIO_4_BY_3
          end
        end

        unless @values['poster_time'].blank?
          poster_time_match = @values['poster_time'].match(/(\d+\.\d+)s/)
          @poster_time = poster_time_match[1].to_f if poster_time_match
        end

        unless @values['image_height'].blank?
          @height = @values['image_height'].to_i
        end

        unless @values['image_width'].blank?
          @width = @values['image_width'].to_i
        end
      end
    end
  end
end