//
//  PikkartVideoPlayer.swift
//  testPKTRecognitionFrameworkApp
//
//  Created by Roberto Avanzi on 16/08/16.
//  Copyright © 2016 Pikkart. All rights reserved.
//

import UIKit

public class PikkartVideoPlayer: NSObject {

    private class VideoOutputShared:AVPlayerItemVideoOutput {
        
        static private var innerSharedInstance:VideoOutputShared!
        
        static func sharedInstance() -> VideoOutputShared {
            if (innerSharedInstance == nil) {
                let settings:[String:AnyObject] = [String(kCVPixelBufferPixelFormatTypeKey):
                                                   Int(kCVPixelFormatType_32BGRA)]
                innerSharedInstance = VideoOutputShared(pixelBufferAttributes:settings)
                innerSharedInstance.suppressesPlayerRendering = true;
            }
            return innerSharedInstance
        }
    }
    
    public enum VIDEO_STATE:Int {
        case REACHED_END = 0
        case PAUSED
        case STOPPED
        case PLAYING
        case READY
        case NOT_READY
        case ERROR
    }
    
    // Constants
    static let  PLAYER_CURSOR_POSITION_MEDIA_START:Float = 0.0
    static let  PLAYER_CURSOR_REQUEST_COMPLETE:Float = -1.0
    static let  PLAYER_VOLUME_DEFAULT:Float = 1.0
    static let  TIMESCALE:Float = 1000;  // 1 millisecond granularity for time
    static let  VIDEO_PLAYBACK_CURRENT_POSITION:Float = 1.0

    // The number of bytes per texel (when using kCVPixelFormatType_32BGRA)
    static let BYTES_PER_TEXEL:Int = 4;
    
    static let StatusKey:String = "status"
    static let TracksKey:String = "tracks"
    static let RateKey:String = "rate"
    static var AVPlayerItemStatusObservationContext:UnsafeMutablePointer<Void> = nil
    static var AVPlayerRateObservationContext:UnsafeMutablePointer<Void> = nil

    private var mediaState:VIDEO_STATE = .NOT_READY
    public  var videoStatus:VIDEO_STATE { return mediaState }
    
    private var requestedCursorPosition:Float = PLAYER_CURSOR_REQUEST_COMPLETE
    private var playerCursorPosition:Float = PLAYER_CURSOR_POSITION_MEDIA_START
    private var playImmediately:Bool = false
    public  var videoSize:CGSize = CGSizeZero
    public  var videoLengthSeconds:Float64 = 0
    private var videoFrameRate:Float = 0
    private var player:AVPlayer?
    public  var videoTextureHandle:GLuint = 0
    
    private var mediaURL:NSURL?
    private var assetReader:AVAssetReader?
    private var assetReaderTrackOutputVideo:AVAssetReaderTrackOutput?
    private var asset:AVURLAsset?
    private var latestSampleBuffer:CMSampleBufferRef?
    private var currentSampleBuffer:CMSampleBufferRef?
    private var dataLock:NSLock = NSLock()
    private var videoOutput:AVPlayerItemVideoOutput?
    private var playerCursorStartPosition:CMTime = CMTime();
    private var currentVolume:Float = 0;

    // used for video creation
    private var lastMissingBufferFrameTime:NSTimeInterval = -1
    private var nbrMissingBufferFrame:UInt = 0
    
    
    private func resetData() {
        
         mediaState = .NOT_READY
         requestedCursorPosition = PikkartVideoPlayer.PLAYER_CURSOR_REQUEST_COMPLETE
         playerCursorPosition = PikkartVideoPlayer.PLAYER_CURSOR_POSITION_MEDIA_START
         playImmediately = false
         videoSize.width = 0.0
         videoSize.height = 0.0
         videoLengthSeconds = 0.0
         videoFrameRate = 0.0
         
         // Remove KVO observers
        if (player != nil) {
            player!.currentItem!.removeObserver(self, forKeyPath: PikkartVideoPlayer.StatusKey)
            player!.removeObserver(self, forKeyPath: PikkartVideoPlayer.RateKey)
        
            NSNotificationCenter.defaultCenter().removeObserver(self, name: AVPlayerItemDidPlayToEndTimeNotification, object:player!.currentItem!)
            NSNotificationCenter.defaultCenter().removeObserver(self, name: AVPlayerItemPlaybackStalledNotification, object:player!.currentItem!)
        }
        
        
        // Release AVPlayer, AVAsset, etc.
        player = nil;
        asset = nil;
        assetReader = nil;
        assetReaderTrackOutputVideo = nil;
        mediaURL = nil;
        
        // Video sample buffer lock
        latestSampleBuffer = nil;
        currentSampleBuffer = nil;
        lastMissingBufferFrameTime = -1;
        nbrMissingBufferFrame = 0;
        
        // Class data lock
        dataLock = NSLock();

    }
    
    //MARK: init method
    override init() {
        
        super.init()
        
        do {
            try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback,
                                                            withOptions: .MixWithOthers)
        } catch let error {
            print("Error during audio configuration \(error)")
        }
        
        resetData()
    }
    
    private func prepareAssetForReading(startTime:CMTime) -> Bool {
        
        let arrayTracks = asset!.tracksWithMediaType(AVMediaTypeAudio)
        
        guard arrayTracks.count == 0 else {
            let assetTrackAudio = arrayTracks[0]
            let audioInputParams = AVMutableAudioMixInputParameters()
            audioInputParams.setVolume(0.1, atTime: playerCursorStartPosition)
            audioInputParams.trackID = assetTrackAudio.trackID
            let audioMix = AVMutableAudioMix()
            audioMix.inputParameters = [audioInputParams]
            player!.currentItem!.audioMix = audioMix
            return true
        }
        
        return false
    }
    
    private func prepareAVPlayer() {
        
        if let realAsset = asset {
            // Create a player item
            let item = AVPlayerItem(asset: realAsset)
            // Add player item status KVO observer
            let opts = NSKeyValueObservingOptions.New;
            item.addObserver(self, forKeyPath: PikkartVideoPlayer.StatusKey, options:opts,
                             context:&PikkartVideoPlayer.AVPlayerItemStatusObservationContext)
            // Create an AV player
            player = AVPlayer(playerItem: item)
            player!.volume = 1.0
            item.addOutput(videoOutput!)
            // Add player rate KVO observer
            player!.addObserver(self, forKeyPath: PikkartVideoPlayer.RateKey, options: opts,
                                context:&PikkartVideoPlayer.AVPlayerRateObservationContext)
            NSNotificationCenter.defaultCenter().addObserver(self, selector:#selector(self.itemDidFinishPlaying(_:)), name: AVPlayerItemDidPlayToEndTimeNotification, object:item)
             NSNotificationCenter.defaultCenter().addObserver(self, selector:#selector(self.itemStalledPlaying(_:)), name: AVPlayerItemPlaybackStalledNotification, object:item)
        }
    }
    
    private func prepareAssetForPlayback() -> Bool {
        // Get video properties
        let videoTracks = asset!.tracksWithMediaType(AVMediaTypeVideo)
        let videoTrack = videoTracks[0];
        videoSize = videoTrack.naturalSize;
        
        videoLengthSeconds = CMTimeGetSeconds(asset!.duration);
        
        // Start playback at time 0.0
        playerCursorStartPosition = kCMTimeZero;
        
        // Start playback at full volume (audio mix level, not system volume level)
        currentVolume = PikkartVideoPlayer.PLAYER_VOLUME_DEFAULT;
        
        // Prepare the AVPlayer to play the audio
        prepareAVPlayer()
        
        // Create asset tracks for reading
        let ret = prepareAssetForReading(playerCursorStartPosition)
        
        if (ret) {
            // Inform our client that the asset is ready to play
            mediaState = .READY;
        }
        
        return ret;
    }
    
    private func loadLocalMedia(url:NSURL) -> Bool {
        var ret = false
        
        asset = AVURLAsset(URL: url)
        
        if ( asset != nil) {
            ret = true
            asset!.loadValuesAsynchronouslyForKeys([PikkartVideoPlayer.TracksKey], completionHandler: {
                NSOperationQueue.mainQueue().addOperationWithBlock({
                    var error:NSError?
                    let status:AVKeyValueStatus = self.asset!.statusOfValueForKey(PikkartVideoPlayer.TracksKey, error: &error)
                    if (status == .Loaded) {
                        // Asset loaded, retrieve info and prepare
                        // for playback
                        self.videoOutput = VideoOutputShared.sharedInstance()
                        if (self.prepareAssetForPlayback() == false) {
                            print("Error - Unable to prepare media for playback")
                            self.mediaState = .ERROR
                        }
                    }
                })
            })
        }
        return ret;
    }
    
    @objc private func itemDidFinishPlaying(notification:NSNotification) {
        mediaState = .REACHED_END
        
        let startTime = CMTime(value: CMTimeValue(PikkartVideoPlayer.PLAYER_CURSOR_POSITION_MEDIA_START * PikkartVideoPlayer.TIMESCALE), timescale: CMTimeScale(PikkartVideoPlayer.TIMESCALE))
        player!.seekToTime(startTime)
    }
    
    @objc private func playAfterStalled(sender:AnyObject) {
        if (mediaState == .PLAYING) {
            player?.play()
        }
    }
    
    @objc private func itemStalledPlaying(notification:NSNotification) {
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.performSelector(#selector(self.playAfterStalled(_:)), withObject: nil, afterDelay: 1.0)
        })
    }

    private func updatePlayerCursorPosition(position:Float) {
        self.playerCursorPosition = position
        self.requestedCursorPosition = position
    }

    //MARK:public methods
    public func load(filename:String, playImmediately:Bool, seekPosition:Float) -> Bool {
        var ret = false
        
        guard mediaState != .NOT_READY &&
              mediaState != .ERROR else {
            let defaultManager = NSFileManager.defaultManager()
            if defaultManager.fileExistsAtPath(filename) {
                mediaURL = NSURL(fileURLWithPath: filename)
            } else {
                mediaURL = NSURL(string: filename);
            }
            
            mediaState = .READY
            self.playImmediately = playImmediately
            if (0.0 <= seekPosition) {
              self.updatePlayerCursorPosition(seekPosition)
            }
            ret = self.loadLocalMedia(mediaURL!)
            if (ret == false) {
                mediaState = .ERROR
            }
            return ret
        }
        
        print("Media already loaded. Unload current media first")
        return ret
    }
    
    public func play(seekposition:Float) -> Bool {
        var ret = false
        
        if (mediaState != .PLAYING &&
            mediaState != .ERROR) {
            
            if (seekposition > 0.0) {
                self.updatePlayerCursorPosition(seekposition)
            }
            dataLock.lock()
            mediaState = .PLAYING
            player!.play()
            dataLock.unlock()
            ret = true
        }
        return ret
    }
    
    public func pause() -> Bool {
        var ret = false
        if (mediaState == .PLAYING) {
            dataLock.lock()
            mediaState = .PAUSED
            player!.pause()
            dataLock.unlock()
            ret = true;
        }
        return ret
    }
    
    public func stop() -> Bool {
        var ret = false
        if (mediaState == .PLAYING) {
            dataLock.lock()
            mediaState = .STOPPED
            player!.pause()
            self.updatePlayerCursorPosition(PikkartVideoPlayer.PLAYER_CURSOR_POSITION_MEDIA_START)
            dataLock.unlock()
            ret = true;
        }
        return ret
    }
    
    public func seekTo(position:Float) -> Bool {
        var ret = false;
        if (mediaState != .ERROR &&
            Float64(position) < self.videoLengthSeconds) {
            self.updatePlayerCursorPosition(position)
            ret = true
        }
        return ret
    }
    
    public func updateVideoData() {
        if (mediaState == .PLAYING) {
            var pixelBufferBaseAddress:UnsafeMutablePointer<UInt8>
            var pixelBuffer:CVPixelBuffer?
            
            let desiredTime = player!.currentItem!.currentTime
            let lastDesiredTime = NSTimeInterval(desiredTime().value)/NSTimeInterval(desiredTime().timescale)
            if (lastDesiredTime < 0 ||
                videoOutput!.hasNewPixelBufferForItemTime(desiredTime()) == false) {
                if (lastMissingBufferFrameTime < 0) {
                    lastMissingBufferFrameTime = lastDesiredTime;
                    nbrMissingBufferFrame = 0;
                } else if (fabs(lastMissingBufferFrameTime - lastDesiredTime) <= 0.1) {
                    nbrMissingBufferFrame += 1
                    lastMissingBufferFrameTime = lastDesiredTime
                }
                if (nbrMissingBufferFrame == 10) {
                    if (self.mediaURL != nil) {
                        let oldURL = self.mediaURL!
                        let oldPlayImmediately = self.playImmediately
                        self.resetData()
                        self.playImmediately = oldPlayImmediately
                        nbrMissingBufferFrame = 0
                        lastMissingBufferFrameTime = -1
                        print("recreate player due to missing buffer for mediaURL\n")
                        self.loadLocalMedia(oldURL)
                    }
                }
                return
            }
            
            pixelBuffer = videoOutput!.copyPixelBufferForItemTime(desiredTime(), itemTimeForDisplay: nil)
            
            if (pixelBuffer == nil) {return}
            
            if (videoTextureHandle == 0) {return}
            
            CVPixelBufferLockBaseAddress(pixelBuffer!, 0)
            pixelBufferBaseAddress=UnsafeMutablePointer<UInt8>(CVPixelBufferGetBaseAddress(pixelBuffer!))
            
            glBindTexture(GLenum(GL_TEXTURE_2D), videoTextureHandle)
            
            let bytesPerRow:size_t = CVPixelBufferGetBytesPerRow(pixelBuffer!)
            
            if (CGFloat(bytesPerRow / PikkartVideoPlayer.BYTES_PER_TEXEL) == videoSize.width) {
                glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(videoSize.width), GLsizei(videoSize.height), 0, GLenum(GL_BGRA), GLenum(GL_UNSIGNED_BYTE), pixelBufferBaseAddress);
            } else {
                glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(videoSize.width), GLsizei(videoSize.height), 0, GLenum(GL_BGRA), GLenum(GL_UNSIGNED_BYTE), nil);
                
                // Now upload each line of texture data as a sub-image
                for i in 0..<Int(videoSize.height) {
                    let line:UnsafeMutablePointer<UInt8> = pixelBufferBaseAddress + i * bytesPerRow;
                    glTexSubImage2D(GLenum(GL_TEXTURE_2D), 0, 0, GLint(i), GLsizei(videoSize.width), 1, GLenum(GL_BGRA), GLenum(GL_UNSIGNED_BYTE), line);
                }
            }
            
            //print("reproduce video at time = \(lastDesiredTime) with VideoSize \(videoSize)\n")
            
            glBindTexture(GLenum(GL_TEXTURE_2D), 0);
            
            // Unlock the buffers
            CVPixelBufferUnlockBaseAddress(pixelBuffer!, 0);
            
        }
    }
    
    //MARK: AVPlayer observation methods
    override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if (context == PikkartVideoPlayer.AVPlayerItemStatusObservationContext) {
            let val:Int = change![NSKeyValueChangeNewKey] as! Int
            let status:AVPlayerItemStatus = AVPlayerItemStatus(rawValue:val)!
            switch status {
                case .Unknown:
                    print("AVPlayerItemStatusObservationContext -> AVPlayerItemStatusUnknown")
                    mediaState = .NOT_READY
                case .ReadyToPlay:
                    print("AVPlayerItemStatusObservationContext -> AVPlayerItemStatusReadyToPlay")
                    if (mediaState != .PLAYING) {
                        mediaState = .READY
                        if (playImmediately == true) {
                            self.play(PikkartVideoPlayer.VIDEO_PLAYBACK_CURRENT_POSITION)
                        }
                    }
                case .Failed:
                    print("AVPlayerItemStatusObservationContext -> AVPlayerItemStatusFailed\n")
                    mediaState = .ERROR
                    let errorLog:AVPlayerItemErrorLog? = player!.currentItem!.errorLog()
                    if (errorLog != nil) {
                        let logString = String(errorLog!.extendedLogData(),
                                               errorLog!.extendedLogDataStringEncoding)
                        print("Error - AVPlayer unable to play media: \(logString)")
                    }
            }
        }
    }
    
    //MARK: deinitializer method
    deinit {
        self.stop()
        self.resetData()
    }
}
