module JamRuby
  class Mix < ActiveRecord::Base
    include S3ManagerMixin

    MAX_MIX_TIME = 7200     # 2 hours

    @@log = Logging.logger[Mix]

    before_destroy :delete_s3_files

    self.primary_key = 'id'

    attr_accessible :ogg_url, :should_retry, as: :admin
    attr_accessor   :is_skip_mount_uploader
    attr_writer     :current_user

    belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :mixes, :foreign_key => 'recording_id'

    validates :download_count, presence: true
    validate :verify_download_count

    skip_callback :save, :before, :store_picture!, if: :is_skip_mount_uploader

    mount_uploader :ogg_url, MixUploader


    def verify_download_count
      if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
        errors.add(:download_count, "must be less than or equal to 100")
      end
    end

    before_validation do
      # this should be an activeadmin only path, because it's using the mount_uploader (whereas the client does something completely different)
      if !is_skip_mount_uploader && ogg_url.present? && ogg_url.respond_to?(:file) && ogg_url_changed?
        self.ogg_length = ogg_url.file.size
        self.ogg_md5 = ogg_url.md5
        self.completed = true
        self.started_at = Time.now
        self.completed_at = Time.now
        # do not set marking_complete = true; use of marking_complete is a client-centric design,
        # and setting to true causes client-centric validations
      end
    end


    def self.schedule(recording)
      raise if recording.nil?

      mix = Mix.new
      mix.is_skip_mount_uploader = true
      mix.recording = recording
      mix.save
      mix[:ogg_url] = construct_filename(recording.created_at, recording.id, mix.id, type='ogg')
      mix[:mp3_url] = construct_filename(recording.created_at, recording.id, mix.id, type='mp3')
      if mix.save
        mix.enqueue
      end
      mix.is_skip_mount_uploader = false

      mix
    end

    def enqueue
      begin
        Resque.enqueue(AudioMixer, self.id, self.sign_put(3600 * 24 * 30, 'ogg'), self.sign_put(3600 * 24 * 30, 'mp3'))
      rescue
        # implies redis is down. we don't update started_at
        false
      end

      # avoid db validations
      Mix.where(:id => self.id).update_all(:started_at => Time.now, :should_retry => false)

      true
    end

    def can_download?(some_user)
      return false if some_user.nil?
      claimed_recording = ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id)

      if claimed_recording
        !claimed_recording.discarded
      else
        false
      end
    end

    def mix_timeout?
      Time.now - started_at > 60 * 30 # 30 minutes to mix is more than enough
    end

    def state
      return 'mixed' if completed
      return 'stream-mix' if recording.has_stream_mix
      return 'waiting-to-mix' if started_at.nil?
      return 'error' if error_count > 0 || mix_timeout?
      return 'mixing'
    end

    def error
      return nil if state != 'error'
      return {error_count: error_count, error_reason: error_reason, error_detail: error_detail} if error_count > 0
      return {error_count: 1, error_reason: 'mix-timeout', error_detail: started_at} if mix_timeout?
      return {error_count: 1, error_reason: 'unknown', error_detail: 'unknown'}
    end

    def too_many_downloads?
      (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
    end

    def errored(reason, detail)
      self.started_at = nil
      self.error_reason = reason
      self.error_detail = detail
      self.error_count = self.error_count + 1
      if self.error_count <= 3
        self.should_retry = true
      end

      save
    end

    def finish(ogg_length, ogg_md5, mp3_length, mp3_md5)
      self.completed_at = Time.now
      self.ogg_length = ogg_length
      self.ogg_md5 = ogg_md5
      self.mp3_length = mp3_length
      self.mp3_md5 = mp3_md5
      self.completed = true
      if save
        Notification.send_recording_master_mix_complete(recording)
      end

      Recording.where(:id => self.recording.id).update_all(:has_final_mix => true)
    end

    # valid for 1 day; because the s3 urls eventually expire
    def manifest
      one_day = 60 * 60 * 24

      jam_track_offset = 0
      jam_track_seek = 0

      was_jamtrack_played = false

      if recording.timeline
        recording_timeline_data = recording.timeline

        # did the jam track play at all?
        jam_track_isplaying = recording_timeline_data["jam_track_isplaying"]
        recording_start_time = recording_timeline_data["recording_start_time"]
        jam_track_play_start_time = recording_timeline_data["jam_track_play_start_time"]
        jam_track_recording_start_play_offset = recording_timeline_data["jam_track_recording_start_play_offset"]

        if jam_track_play_start_time != 0
          was_jamtrack_played = true

          # how long did the JamTrack play? not needed because we limit on the input tracks, which represents how long the recording is, too
          jam_track_play_time = recording_timeline_data["jam_track_play_time"]


          offset = jam_track_play_start_time - recording_start_time

          @@log.debug("base offset = #{offset}")
          if offset >= 0
            # jamtrack started after recording, so buffer with silence as necessary\

            if jam_track_recording_start_play_offset < 0
              @@log.info("prelude captured. offsetting further by #{-jam_track_recording_start_play_offset}")
              # a negative jam_track_recording_start_play_offset indicates prelude, i.e., silence
              # so add it to the offset to add more silence as necessary
              offset = offset + -jam_track_recording_start_play_offset
              jam_track_offset = offset
            else
              @@log.info("positive jamtrack offset; seeking into jamtrack by #{jam_track_recording_start_play_offset}")
              # a positive jam_track_recording_start_play_offset means we need to cut into the jamtrack
              jam_track_seek = jam_track_recording_start_play_offset
              jam_track_offset = offset
            end
          else
            # jamtrack started before recording, so we can seek into it to make up for the missing parts

            if jam_track_recording_start_play_offset < 0
              @@log.info("partial prelude captured. offset becomes jamtrack offset#{-jam_track_recording_start_play_offset}")
              # a negative jam_track_recording_start_play_offset indicates prelude, i.e., silence
              # so add it to the offset to add more silence as necessary
              jam_track_offset = -jam_track_recording_start_play_offset
            else
              @@log.info("no prelude captured. offset becomes jamtrack offset=#{jam_track_recording_start_play_offset}")

              jam_track_offset = 0
              jam_track_seek = jam_track_recording_start_play_offset
            end


            # also, ignore jam_track_recording_start_play_offset - it simply matches the offset in this case
          end

          @@log.info("computed values. jam_track_offset=#{jam_track_offset} jam_track_seek=#{jam_track_seek}")


        end
      end

      manifest = { "files" => [], "timeline" => [] }
      mix_params = []


      # this 'pick limiter' logic will ensure that we set a limiter on the 1st recorded_track we come across.
      pick_limiter = false
      if was_jamtrack_played
        # we only use the limiter feature if this is a JamTrack recording
        # by setting this to true, the 1st recorded_track in the database will be the limiter
        pick_limiter = true
      end

      recording.recorded_tracks.each do |recorded_track|
        manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0, limiter:pick_limiter }
        pick_limiter = false
        mix_params << { "level" => 1.0, "balance" => 0 }
      end

      recording.recorded_backing_tracks.each do |recorded_backing_track|
        manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
        mix_params << { "level" => 1.0, "balance" => 0 }
      end

      if was_jamtrack_played
        recording.recorded_jam_track_tracks.each do |recorded_jam_track_track|
          manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day, sample_rate=44), "codec" => "vorbis", "offset" => jam_track_offset, "seek" => jam_track_seek }
          # let's look for level info from the client
          level = 1.0 # default value - means no effect
          if recorded_jam_track_track.timeline

            timeline_data = recorded_jam_track_track.timeline

            # always take the 1st entry for now
            first = timeline_data[0]

            if first["mute"]
              # mute equates to no noise
              level = 0.0
            else
              # otherwise grab the left channel...
              level = first["vol_l"]
            end
          end

          mix_params << { "level" => level, "balance" => 0 }
        end
      end

      manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
      manifest["output"] = { "codec" => "vorbis" }
      manifest["recording_id"] = self.recording.id
      manifest
    end


    def local_manifest
      remote_manifest = self.manifest
      remote_manifest["files"].each do |file|
        filename = file["filename"]

        basename = File.basename(filename)
        basename = basename[0..(basename.index('?') - 1)]

        file["filename"] = basename
      end

      # update manifest so that audiomixer writes here
      remote_manifest["output"]["filename"] = 'out.ogg'
      # update manifest so that audiomixer writes here
      remote_manifest["error_out"] = 'error.out'
      remote_manifest["mix_id"] = self.id
      remote_manifest
    end

    def download_script
      out = ''

      remote_manifest = manifest
      remote_manifest["files"].each do |file|
        filename = file["filename"]
        basename = File.basename(filename)
        basename = basename[0..(basename.index('?') - 1)]

        out << "curl -o \"#{basename}\" \"#{filename}\"\r\n\r\n"
      end
      out << "\r\n\r\n"
      out
    end

    def s3_url(type='ogg')
      if type == 'ogg'
        s3_manager.s3_url(self[:ogg_url])
      else
        s3_manager.s3_url(self[:mp3_url])
      end
    end
    
    def is_completed
      completed
    end

    # if the url starts with http, just return it because it's in some other store.  Otherwise it's a relative path in s3 and needs be signed
    def resolve_url(url_field, mime_type, expiration_time, secure = true)
      self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => secure})
    end

    def sign_url(expiration_time = 120, type='ogg', secure = true)
      type ||= 'ogg'
      # expire link in 1 minute--the expectation is that a client is immediately following this link
      if type == 'ogg'
        resolve_url(:ogg_url, 'audio/ogg', expiration_time, secure)
      else
        resolve_url(:mp3_url, 'audio/mpeg', expiration_time, secure)
      end
    end

    def sign_put(expiration_time = 3600 * 24, type='ogg')
      type ||= 'ogg'
      if type == 'ogg'
        s3_manager.sign_url(self[:ogg_url], {:expires => expiration_time, :content_type => 'audio/ogg', :secure => false}, :put)
      else
        s3_manager.sign_url(self[:mp3_url], {:expires => expiration_time, :content_type => 'audio/mpeg', :secure => false}, :put)
      end
    end


    def filename(type='ogg')
      # construct a path for s3
      Mix.construct_filename(recording.created_at, self.recording_id, self.id, type)
    end

    def update_download_count(count=1)
      self.download_count = self.download_count + count
      self.last_downloaded_at = Time.now
    end


    def delete_s3_files
      s3_manager.delete(filename(type='ogg')) if self[:ogg_url] && s3_manager.exists?(filename(type='ogg'))
      s3_manager.delete(filename(type='mp3')) if self[:mp3_url] && s3_manager.exists?(filename(type='mp3'))
    end

    private



    def self.construct_filename(created_at, recording_id, id, type='ogg')
      raise "unknown ID" unless id
      "recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/mix-#{id}.#{type}"
    end
  end
end
