/**************************************************************
 * 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * 
 *************************************************************/


#include "macavf_player.hxx"
#include "macavf_framegrabber.hxx"
#include "macavf_window.hxx"

#include <cmath> // for log10()

using namespace ::com::sun::star;

#include <hash_map>
typedef std::hash_map<NSObject*,avmedia::macavf::MacAVObserverHandler*> HandlersForObject;

@implementation MacAVObserverObject
{
    HandlersForObject maHandlersForObject;
}
- (void)observeValueForKeyPath:(NSString*)pKeyPath ofObject:(id)pObject change:(NSDictionary*)pChangeDict context:(void*)pContext
{
    NSString* pDictStr = [NSString stringWithFormat:@"%@", pChangeDict];
    OSL_TRACE( "MacAVObserver::onKeyChange k=\"%s\" c=%s", [pKeyPath UTF8String], [pDictStr UTF8String]);
    avmedia::macavf::MacAVObserverHandler* pHandler = (avmedia::macavf::MacAVObserverHandler*)pContext;
    pHandler->handleObservation( pKeyPath );
}

- (void)onNotification:(NSNotification*)pNotification
{
    NSString* pNoteName = (NSString*)[pNotification name];
    OSL_TRACE( "MacAVObserver::onNotification key=\"%s\"", [pNoteName UTF8String]);
    HandlersForObject::iterator it = maHandlersForObject.find( [pNotification object]);
    if( it != maHandlersForObject.end() )
        (*it).second->handleObservation( pNoteName );
}

- (void)setHandlerForObject:(NSObject*)pObject handler:(avmedia::macavf::MacAVObserverHandler*)pHandler
{
    maHandlersForObject[ pObject] = pHandler;
}

- (void)removeHandlerForObject:(NSObject*)pObject
{
    maHandlersForObject.erase( pObject);
}

@end


namespace avmedia { namespace macavf {

MacAVObserverObject* MacAVObserverHandler::mpMacAVObserverObject = NULL;
 
MacAVObserverObject* MacAVObserverHandler::getObserver() const
{
    if( !mpMacAVObserverObject)
    {
        mpMacAVObserverObject = [MacAVObserverObject alloc];
        [mpMacAVObserverObject retain];
    }
    return mpMacAVObserverObject;
}


// ----------------
// - Player -
// ----------------

Player::Player( const uno::Reference< lang::XMultiServiceFactory >& rxMgr )
:   mxMgr( rxMgr )
,   mpPlayer( NULL )
,   mfUnmutedVolume( 0 )
,   mfStopTime( DBL_MAX )
,   mbMuted( false )
,   mbLooping( false )
{}

// ------------------------------------------------------------------------------

Player::~Player()
{
    if( !mpPlayer )
        return;
    // remove the observers
    [mpPlayer removeObserver:getObserver() forKeyPath:@"currentItem.status"];
    AVPlayerItem* pOldPlayerItem = [mpPlayer currentItem];
    [[NSNotificationCenter defaultCenter] removeObserver:getObserver()
        name:AVPlayerItemDidPlayToEndTimeNotification
        object:pOldPlayerItem];
    [getObserver() removeHandlerForObject:pOldPlayerItem];
    // release the AVPlayer
    CFRelease( mpPlayer );
}

// ------------------------------------------------------------------------------

bool Player::handleObservation( NSString* pKeyPath )
{
    OSL_TRACE( "AVPlayer::handleObservation key=\"%s\"", [pKeyPath UTF8String]);
    if( [pKeyPath isEqualToString:AVPlayerItemDidPlayToEndTimeNotification])
    {
        OSL_TRACE( "AVPlayer replay=%d", mbLooping);
        if( mbLooping )
            setMediaTime( 0.0);
    }
    return true;
}

// ------------------------------------------------------------------------------

bool Player::create( const ::rtl::OUString& rURL )
{
    // get the media asset
    NSString* aNSStr = [NSString stringWithCharacters:rURL.getStr() length:rURL.getLength()];
    NSURL* aNSURL = [NSURL URLWithString: [aNSStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
    // get the matching AVPlayerItem
    AVPlayerItem* pPlayerItem = [AVPlayerItem playerItemWithURL:aNSURL];

    // create or update the AVPlayer with the new AVPlayerItem
    if( !mpPlayer )
    {
        mpPlayer = [AVPlayer playerWithPlayerItem:pPlayerItem];
        CFRetain( mpPlayer );
        [mpPlayer setActionAtItemEnd:AVPlayerActionAtItemEndNone];
    }
    else
    {
        // remove the obsoleted observers
        AVPlayerItem* pOldPlayerItem = [mpPlayer currentItem];
        [mpPlayer removeObserver:getObserver() forKeyPath:@"currentItem.status"];
        [getObserver() removeHandlerForObject:pOldPlayerItem];
        [[NSNotificationCenter defaultCenter] removeObserver:getObserver()
            name:AVPlayerItemDidPlayToEndTimeNotification
            object:pOldPlayerItem];
        // replace the playeritem
        [mpPlayer replaceCurrentItemWithPlayerItem:pPlayerItem];
    }

    // observe the status of the current player item
    [mpPlayer addObserver:getObserver() forKeyPath:@"currentItem.status" options:0 context:this];

    // observe playback-end needed for playback looping
    [[NSNotificationCenter defaultCenter] addObserver:getObserver()
        selector:@selector(onNotification:)
        name:AVPlayerItemDidPlayToEndTimeNotification
        object:pPlayerItem];
    [getObserver() setHandlerForObject:pPlayerItem handler:this];

    return true;
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::start()
    throw (uno::RuntimeException)
{
    if( !mpPlayer )
        return;
#if 0
    const AVPlayerStatus eStatus = [mpPlayer status];
    OSL_TRACE ("Player::start status=%d", (int)eStatus);
    if( eStatus == AVPlayerStatusReadyToPlay)
#endif
        [mpPlayer play];
    // else // TODO: delay until it becomes ready
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::stop()
    throw (uno::RuntimeException)
{
    if( !mpPlayer )
        return;
    const bool bPlaying = isPlaying();
    OSL_TRACE ("Player::stop() playing=%d", bPlaying);
    if( bPlaying )
        [mpPlayer pause];
}

// ------------------------------------------------------------------------------

sal_Bool SAL_CALL Player::isPlaying()
    throw (uno::RuntimeException)
{
    if( !mpPlayer )
        return false;
    const float fRate = [mpPlayer rate];
    return (fRate != 0.0);
}

// ------------------------------------------------------------------------------

double SAL_CALL Player::getDuration()
    throw (uno::RuntimeException)
{
    // slideshow checks for non-zero duration, so cheat here
    double duration = 0.01;

    if( mpPlayer )
    {
        AVPlayerItem* pItem = [mpPlayer currentItem];
        if( [pItem status] == AVPlayerItemStatusReadyToPlay )
            duration = CMTimeGetSeconds( [pItem duration] );
        else // fall back to AVAsset's best guess
            duration = CMTimeGetSeconds( [[pItem asset] duration] );
    }

    return duration;
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::setMediaTime( double fTime )
    throw (uno::RuntimeException)
{
    OSL_TRACE ("Player::setMediaTime( %.3fsec)", fTime);
    if( mpPlayer )
        [mpPlayer seekToTime: CMTimeMakeWithSeconds(fTime,1000) ];
}

// ------------------------------------------------------------------------------

double SAL_CALL Player::getMediaTime()
    throw (uno::RuntimeException)
{
    if( !mpPlayer )
        return 0.0;

    const double position = CMTimeGetSeconds( [mpPlayer currentTime] );
    OSL_TRACE( "Player::getMediaTime() = %.3fsec", position);
    if( position >= mfStopTime )
        if( isPlaying() )
            stop();

    return position;
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::setStopTime( double fTime )
    throw (uno::RuntimeException)
{
    OSL_TRACE ("Player::setStopTime( %.3fsec)", fTime);
    mfStopTime = fTime;
}

// ------------------------------------------------------------------------------

double SAL_CALL Player::getStopTime()
    throw (uno::RuntimeException)
{
    return mfStopTime;
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::setRate( double fRate )
    throw (uno::RuntimeException)
{
    OSL_TRACE ("Player::setRate( %.3f)", fRate);
    if( !mpPlayer )
        return;

    // playback rate: 0 = stop, 1 = normal speed, 2 = double speed, -1 = normal speed backwards
    [mpPlayer setRate: fRate];
}

// ------------------------------------------------------------------------------

double SAL_CALL Player::getRate()
    throw (uno::RuntimeException)
{
    // macavf: 0 = stop, 1 = normal speed, 2 = double speed, -1 = normal speed backwards
    const double fRate = mpPlayer ? (double)[mpPlayer rate] : 1.0;
    OSL_TRACE ("Player::getRate() = %.3f", fRate);
    return fRate;
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::setPlaybackLoop( sal_Bool bSet )
    throw (uno::RuntimeException)
{
    OSL_TRACE ("Player::setPlaybackLoop( %d)", bSet );
    mbLooping = bSet;
}

// ------------------------------------------------------------------------------

sal_Bool SAL_CALL Player::isPlaybackLoop()
    throw (uno::RuntimeException)
{
    const bool bRet = mbLooping;
    OSL_TRACE ("Player::isPlaybackLoop() = %d", bRet );
    return bRet;
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::setMute( sal_Bool bSet )
    throw (uno::RuntimeException)
{
    OSL_TRACE( "Player::setMute(%d), was-muted: %d unmuted-volume: %.3f", bSet, mbMuted, mfUnmutedVolume );

    if( !mpPlayer )
        return;

    mbMuted = (bSet == TRUE);
    [mpPlayer setMuted:mbMuted]; 
}

// ------------------------------------------------------------------------------

sal_Bool SAL_CALL Player::isMute()
    throw (uno::RuntimeException)
{
    OSL_TRACE ("Player::isMuted() = %d", mbMuted);
    return mbMuted;
}

// ------------------------------------------------------------------------------

void SAL_CALL Player::setVolumeDB( sal_Int16 nVolumeDB ) 
	throw (uno::RuntimeException)
{
    // -40dB <-> AVPlayer volume 0.0
    //   0dB <-> AVPlayer volume 1.0
    mfUnmutedVolume = (nVolumeDB <= -40) ? 0.0 : pow( 10.0, nVolumeDB / 20.0 );
    OSL_TRACE( "Player::setVolume(%ddB), muted=%d, unmuted-volume: %.3f", nVolumeDB, mbMuted, mfUnmutedVolume );

    // change volume
    if( !mbMuted && mpPlayer )
        [mpPlayer setVolume:mfUnmutedVolume];
}

// ------------------------------------------------------------------------------
    
sal_Int16 SAL_CALL Player::getVolumeDB() 
	throw (uno::RuntimeException)
{
    if( !mpPlayer )
        return 0;

    // get the actual volume
    const float fVolume = [mpPlayer volume];

    // convert into Dezibel value
    // -40dB <-> AVPlayer volume 0.0
    //   0dB <-> AVPlayer volume 1.0
    const int nVolumeDB = (fVolume <= 0) ? -40 : lrint( 20.0*log10(fVolume));
    
    return (sal_Int16)nVolumeDB;
}

// ------------------------------------------------------------------------------

awt::Size SAL_CALL Player::getPreferredPlayerWindowSize()
    throw (uno::RuntimeException)
{
    awt::Size aSize( 0, 0 ); // default size

    AVAsset* pMovie = [[mpPlayer currentItem] asset];
    NSArray* pVideoTracks = [pMovie tracksWithMediaType:AVMediaTypeVideo];
    AVAssetTrack* pFirstVideoTrack = (AVAssetTrack*)[pVideoTracks firstObject];
    if( pFirstVideoTrack )
    {
        const CGSize aPrefSize = [pFirstVideoTrack naturalSize];
        aSize = awt::Size( aPrefSize.width, aPrefSize.height );
    }

    return aSize;
}

// ------------------------------------------------------------------------------

uno::Reference< ::media::XPlayerWindow > SAL_CALL Player::createPlayerWindow( const uno::Sequence< uno::Any >& aArguments )
    throw (uno::RuntimeException)
{
    // get the preferred window size
    const awt::Size aSize( getPreferredPlayerWindowSize() );
    OSL_TRACE( "Player::createPlayerWindow %dx%d argsLength: %d", aSize.Width, aSize.Height, aArguments.getLength() );

    // get the parent view
    sal_IntPtr nNSViewPtr = NULL;
    aArguments[0] >>= nNSViewPtr;
    NSView* pParentView = reinterpret_cast<NSView*>(nNSViewPtr);

    // check the window parameters
    uno::Reference< ::media::XPlayerWindow > xRet;
    if( (aSize.Width <= 0) || (aSize.Height <= 0) || (pParentView == NULL) )
         return xRet;

    // create the window
    ::avmedia::macavf::Window* pWindow = new ::avmedia::macavf::Window( mxMgr, *this, pParentView );
    xRet = pWindow;
    return xRet;
}

// ------------------------------------------------------------------------------

uno::Reference< media::XFrameGrabber > SAL_CALL Player::createFrameGrabber() 
    throw (uno::RuntimeException)
{
    uno::Reference< media::XFrameGrabber > xRet;
    OSL_TRACE ("Player::createFrameGrabber");

    FrameGrabber* pGrabber = new FrameGrabber( mxMgr );
    AVAsset* pMovie = [[mpPlayer currentItem] asset];
    if( pGrabber->create( pMovie ) )
        xRet = pGrabber;

    return xRet;
}

// ------------------------------------------------------------------------------

::rtl::OUString SAL_CALL Player::getImplementationName(  )
    throw (uno::RuntimeException)
{
    return ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM( AVMEDIA_MACAVF_PLAYER_IMPLEMENTATIONNAME ) );
}

// ------------------------------------------------------------------------------

sal_Bool SAL_CALL Player::supportsService( const ::rtl::OUString& ServiceName )
    throw (uno::RuntimeException)
{
    return ServiceName.equalsAsciiL( RTL_CONSTASCII_STRINGPARAM ( AVMEDIA_MACAVF_PLAYER_SERVICENAME ) );
}

// ------------------------------------------------------------------------------

uno::Sequence< ::rtl::OUString > SAL_CALL Player::getSupportedServiceNames(  )
    throw (uno::RuntimeException)
{
    uno::Sequence< ::rtl::OUString > aRet(1);
    aRet[0] = ::rtl::OUString( RTL_CONSTASCII_USTRINGPARAM ( AVMEDIA_MACAVF_PLAYER_SERVICENAME ) );

    return aRet;
}

} // namespace macavf
} // namespace avmedia

