//-----------------------------------------------------------------------------
// Copyright (c) 2013 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
#ifdef TORQUE_ALLOW_GAMEKIT
#import "GameCenter.h"

#include "platformiOS.h"
#include "string/stringBuffer.h"
#include "sim/simBase.h"

extern iOSPlatState platState;

IMPLEMENT_CONOBJECT(Achievement);

// Blank constructor for Achievement class
Achievement::Achievement()
{
}

// Called when declared in script or registered
bool Achievement::onAdd()
{
    // Perform onAdd routine from parent class(es)
    if (!Parent::onAdd())
        return false;
    
    // Add to named AchievementSet
    // This set is available from anywhere in TorqueScript as AchievementSet
    Sim::getAchievementSet()->addObject(this);

    // Call onAdd in script!
    Con::executef(this, 2, "onAdd", Con::getIntArg(getId()));
    
    // Everything went well, return true
    return true;
}

// Called when deleted from script
void Achievement::onRemove()
{
    // We call this on this objects namespace so we unlink them after
    // Call onRemove in script!
    Con::executef(this, 2, "onRemove", Con::getIntArg(getId()));
    
    Parent::onRemove();
}

// Expose all the Achievement fields to TorqueScript
void Achievement::initPersistFields()
{
    // The following member variables are the iT2D equivalent of what is found in Game Center
    // It is important to note that the actual properties in Game Center are read-only
    
    addField("identifier", TypeString, Offset(mIdentifier, Achievement), "A unique string used to identify the achievement");
    addField("title", TypeString, Offset(mTitle, Achievement), "Achievement Title");
    addField("achievedDescription", TypeString, Offset(mAchievedDescription, Achievement), "A localized description of the completed achievement");
    addField("unachievedDescription", TypeString, Offset(mUnachievedDescription, Achievement), "A localized description of the achievement, to be used when the achievement has not been completed");
    addField("hidden", TypeBool, Offset(mHidden, Achievement), "A Boolean value that states whether this achievement should be visible to players");
    addField("maximumPoints", TypeS32, Offset(mMaximumPoints, Achievement), "The number of points earned by completing this achievement");
}

// Helper function used to set all the achievement fields
// This is only useful when creating a Achievement in C++
// Currently the only example of this is used in cacheAchievements,
// converting a GKAchievementDescription into a Achievement
void Achievement::setAchievementFields(const char* identifier, const char* title, const char* achievedDescription, const char* unachievedDescription, int maxPoints, bool hidden)
{
    mIdentifier = StringTable->insert(identifier);
    mTitle = StringTable->insert(title);
    mAchievedDescription = StringTable->insert(achievedDescription);
    mUnachievedDescription = StringTable->insert(unachievedDescription);
    mMaximumPoints = maxPoints;
    mHidden = hidden;
}

// Print all the Achievement fields to the console
// This is only useful for debugging purposes
void Achievement::printAchievement()
{
    Con::printf("Achievement identifier: %s", mIdentifier);
    Con::printf("Achievement title: %s", mTitle);
    Con::printf("Achievement description: %s", mAchievedDescription);
    Con::printf("Achievement un-description: %s", mUnachievedDescription);
    Con::printf("Achievement maximum points: %i", mMaximumPoints);
    Con::printf("Achievement hidden: %i", mHidden);
}

// Exposes the Achievement::printAchievement() function to TorqueScript
// only useful for debugging purposes
ConsoleMethod(Achievement, printAchievement, void, 2, 2, "() Prints out the properties of an achievement")
{
    object->printAchievement();
}

// Utility function that will determine whether the current iOS support Game Center
BOOL isGameCenterAvailable()
{
    // Build a class from a string, using a Game Center specific class: GKLocalPlayer
    // http://developer.apple.com/library/ios/#documentation/GameKit/Reference/GKLocalPlayer_Ref/Reference/Reference.html
    Class gcClass = (NSClassFromString(@"GKLocalPlayer"));
    
    // This build of iT2D is shooting for a minimum of iOS 4.1
    NSString *reqSysVer = @"4.1";
    NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
    
    BOOL osVersionSupported = ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending);
    
    return (gcClass && osVersionSupported);
}

// Primary Game Center Objective-C starts here
@implementation GameCenterManager

@synthesize achievementCache;
@synthesize gameCenterViewController;
@synthesize achievementCount;


// Main initialization function for GameCenterManager
// This is exposed to TorqueScript via the initGameCenter() function
- (id) init
{
    self = [super init];
    
    if(self != NULL)
    {
        gameCenterAvailable = NO;
        isAuthenticated = NO;
        achievementCount = 0;
        achievementCache = NULL;
        gameCenterViewController = [[UIViewController alloc] init];
        gameCenterViewController.view.hidden = true;

    }
    
    return self;
}

// Main cleanup function for GameCenterManager
// This is exposed to TorqueScript via the closeGameCenter() function
- (void) dealloc
{
    // TODO - Call this from script
    gameCenterAvailable = NO;
    isAuthenticated = NO;
    
    self.achievementCache = NULL;
    
    if(gameCenterViewController != NULL)
        [gameCenterViewController release];
    
    [super dealloc];
}

// This is the second part of initializing Game Center. The init function simply allocates memory.
// This function actually connects and authenticates the current user
// Exposed to TorqueScript via the initGameCenter() function
- (void) authenticateLocalUser
{
    // Assume the player is not currently authenticated
    isAuthenticated = NO;
    
    // Check to see if this iOS supports Game Center
	gameCenterAvailable = isGameCenterAvailable();
	
    // Game Center not supported. Abort and print the error
	if(!gameCenterAvailable)
    {
        Con::printf("Failed to authenticate player. This iOS does not support Game Center");
		return;
	}
	
    // The iOS supports Game Center, so attempt to authenticate
    [[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:^(NSError *error)
    {
        // Something went wrong with the authentication
        // Find out what the error is and print it to the console
		if (error != nil)
        {
            const char* errorMessage = [[error localizedDescription] UTF8String];
            Con::printf("Failed to authenticate player. %s", errorMessage);
            
            // Let the scripts know. 0 means it failed
            Con::executef(2, "gameCenterAuthenticationChanged", "0");
        }
        else
        {
            // Successful authentication occurred
			isAuthenticated = YES;
            
            // Start listening for changes in authentication
			[self registerForAuthenticationNotification:error];
			
            // Cache the achievements
			[self cacheAchievements];
            
			// Let the scripts know. 1 means it succeeded
			Con::executef(2, "gameCenterAuthenticationChanged", "1");
		}
	}];
}

// Helper function. This watches for any changes to the authentication for the Local Player 
- (void)registerForAuthenticationNotification: (NSError*) error
{
    if(error == NULL)
	{
        NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
        
        [nc addObserver:self selector:@selector(authenticationChanged) name:GKPlayerAuthenticationDidChangeNotificationName object:nil];

		Con::printf("Game Center Authentication has changed");
	}
}

// Signaled by registerForAuthenticationNotification
// This function will find out how the authentication changed
- (void) authenticationChanged
{
    gameCenterAvailable = isGameCenterAvailable();
        
    if(!gameCenterAvailable)
    {
        Con::printf("Authentication failed: Game Center is not initialized or supported on this iOS.");
        return;
    }
    
    // Check to see if player is still authenticated
    if([GKLocalPlayer localPlayer].authenticated == YES)
    {
        isAuthenticated = YES;
        
        Con::printf("Authentication changed successfully");
        Con::executef(2, "gameCenterAuthenticationChanged", "1");
    }
    else
    {
        Con::printf("Authentication failed for Local Player");
        Con::executef(2, "gameCenterAuthenticationChanged", "0");
    }
}

// Reload all the scores for a specific leaderboard (determined by category)
// Exposed to TorqueScript via reloadHighScores(%category);
- (void) reloadHighScoresForCategory:(NSString *)category
{
    GKLeaderboard* leaderBoard = [[[GKLeaderboard alloc] init] autorelease];
    
    leaderBoard.playerScope = GKLeaderboardPlayerScopeGlobal;
    leaderBoard.category = category;
    leaderBoard.timeScope = GKLeaderboardTimeScopeAllTime;
    leaderBoard.range = NSMakeRange(1, 15);
    
    [leaderBoard loadScoresWithCompletionHandler:^(NSArray *scores, NSError *error) 
    {
        if(error != nil)
        {
            const char* errorMessage = [[error localizedDescription] UTF8String];
            Con::printf("Failed to reload highscores: %s", errorMessage);
            
            // Perform custom error handling here
        }
        else
        {
            const char* board = [category UTF8String];
            Con::printf("Highscores successfully reloaded for %s", board);
            
            // Perform success code here
        }
    }];
}

// Responsible for reporting a new score to a leaderboard (determined by category)
// Exposed to TorqueScript via reportScore(%score, %category);
- (void) reportScore:(int64_t)score forCategory:(NSString *)category
{
    GKScore* scoreReporter = [[[GKScore alloc] initWithCategory:category] autorelease];
    
    scoreReporter.value = score;
    
    [scoreReporter reportScoreWithCompletionHandler:^(NSError *error) 
    {
        if(error != nil)
        {
            const char* errorMessage = [[error localizedDescription] UTF8String];
            Con::printf("Failed to submit score. %s", errorMessage);
        }
        else
        {
            [self reloadHighScoresForCategory:category];
            const char* board = [category UTF8String];
            Con::printf("Score successfully submitted to %s", board);
            
            // Perform custom success code here (saving, notification, etc)
        }
}];
}

// Displays the leaderboard using Game Center's interface
- (void) showLeaderboard
{
    // Check to see if local player is signed in
    // If not, error out and exit function
    if([GKLocalPlayer localPlayer].authenticated == NO)
    {
        Con::printf("Failed to show leaderboard: Local Player not authenticated");
        return;
    }
            
    GKLeaderboardViewController *leaderBoardController = [[GKLeaderboardViewController alloc] init];
        
    if(leaderBoardController != nil)
    {
        leaderBoardController.leaderboardDelegate = self;
            
        UIWindow* window = [UIApplication sharedApplication].keyWindow;

        [window addSubview: gameCenterViewController.view];

        [gameCenterViewController presentModalViewController:leaderBoardController animated:YES];
    }
}

// Called when the user finishes viewing the Leader Board view
- (void) leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController
{
    [gameCenterViewController dismissModalViewControllerAnimated:YES];
    [viewController.view removeFromSuperview];
    [viewController release];
}

// Responsible for submitting achievement progress
// identifier is the unique string for the achievement
// percentComplete determines how far the user has progressed to unlocking the achievement
// Exposed to TorqueScript via submitAchievement(%identifier, %percentComplete);
- (void) submitAchievement:(NSString *)identifier percentComplete:(float)percentComplete
{

    GKAchievement* achievement = [self.achievementCache objectForKey:identifier];
        
    if(achievement != NULL)
    {
        if((achievement.percentComplete >= 100.0) || (achievement.percentComplete >= percentComplete))
        {
            achievement = NULL;
            achievement.percentComplete = percentComplete;
        }
    }
    else
    {
        achievement = [[[GKAchievement alloc] initWithIdentifier:identifier] autorelease];
        achievement.percentComplete = percentComplete;
            
        [self.achievementCache setObject:achievement forKey:achievement.identifier];
    }
        
    if(achievement != NULL)
    {
        [achievement reportAchievementWithCompletionHandler:^(NSError *error)
        {
			if (error != nil)
            {
				const char* errorMessage = [[error localizedDescription] UTF8String];
                Con::printf("Failed to submit achievement: %s", errorMessage);
                
                // Perform custom error handling here
			}
            else
            {
                const char* achievementID = [identifier UTF8String];
                Con::printf("Achievement %s successfully submitted", achievementID);
                
                // Perform custom success code here (saving, notification, etc)
            }
		}];
    }
}
// Resets all achievement progress for a user
// Exposed to TorqueScript via resetAchievements();
- (void) resetAchievements
{
    self.achievementCache = NULL;
    [GKAchievement resetAchievementsWithCompletionHandler:^(NSError *error) 
    {
        if(error != nil)
        {
            const char* errorMessage = [[error localizedDescription] UTF8String];
            Con::printf("Failed to submit achievement: %s", errorMessage);
            
            // Perform custom error handling here
        }
        else
        {
            Con::printf("Achievements reset");
            
            // Perform custom success handling here
        }
    }];
}

// Displays the achievements using Game Center's view
- (void)showAchievements
{	
    // Check to see if local player is signed in
    // If not, error out and exit function
	if([GKLocalPlayer localPlayer].authenticated == NO)
    {
        Con::printf("Failed to show achievements: Local Player not authenticated");
        return;
    }
	
    GKAchievementViewController *achievements = [[GKAchievementViewController alloc] init];

    if (achievements != nil)
    {
        achievements.achievementDelegate = self;
		
		UIWindow* window = [UIApplication sharedApplication].keyWindow;
		[window addSubview: gameCenterViewController.view];
        
        [gameCenterViewController presentModalViewController: achievements animated: YES];
    }	
}

// Called when the user finishes viewing the achievements view
- (void)achievementViewControllerDidFinish:(GKAchievementViewController *)viewController
{
    [gameCenterViewController dismissModalViewControllerAnimated:YES];
	[viewController.view removeFromSuperview];
	[viewController release];
}

// This extremely important function is used for grabbing all the achievements this app supports
// and exposing it to TorqueScript. This will create new t2DAchievements and populate them
// with data accessed from iTunes Connect. This will only work if you have an iTunes Connect
// account setup, along with a registered app with achievements.
//
// For more information on iTunes Connect, see the following two links:
// iT2D Official Documentation: http://docs.garagegames.com/it2d/official
// iTunes Connect Doc: https://itunesconnect.apple.com/docs/iTunesConnect_DeveloperGuide.pdf
- (void)cacheAchievements
{
    if(self.achievementCache == NULL)
    {
        [GKAchievement loadAchievementsWithCompletionHandler:^(NSArray *achievements, NSError *error) 
         {
             if(error != nil)
             {
                 const char* errorMessage = [[error localizedDescription] UTF8String];
                 Con::printf("Failed to cache achievements. %s", errorMessage);
                 
                 // Perform custom caching code here (loading from file, retrying, etc)
             }
             else
             {
                 NSMutableDictionary* tempCache = [NSMutableDictionary dictionaryWithCapacity: [achievements count]];
                 for(GKAchievement* achievement in achievements)
                 {
                     Con::printf("Achievement progres: %f", achievement.percentComplete);
                     [tempCache setObject: achievement forKey: achievement.identifier];
                 }
                 self.achievementCache = tempCache;
             }
         }];
    }
    [GKAchievementDescription loadAchievementDescriptionsWithCompletionHandler:
     ^(NSArray *descriptions, NSError *error) 
    {
        // Something went wrong when trying to access the achievement list
        // The error should contain some information, so print the localized string
        if (error != nil)
        {
            const char* errorMessage = [[error localizedDescription] UTF8String];
            Con::printf("Failed to cache achievements. %s", errorMessage);
        }
        else
        {
            // If there are no achievements, the array will be nil
            // Otherwise, start iteration through them
            if (descriptions != nil)
            {
                // For each individual achievement in the array
                for(GKAchievementDescription* description in descriptions)
                {
                    // Convert the GKAchievementDescription fields to iT2D compatible versions
                    const char* title = [description.title UTF8String];
                    const char* identifier = [description.identifier UTF8String];
                    const char* achievedDescription = [description.achievedDescription UTF8String];
                    const char* unachieveDescription = [description.unachievedDescription UTF8String];
                    int maxPoints = description.maximumPoints;
                    bool hidden = description.hidden;
                    
                    // First, check to see if this is a duplicate or not
                    // If it is, skip adding this
                    if(Sim::getAchievementSet()->findObject(identifier))
                    {
                        // Do not allocate memory or add this achievement
                        // it already exists in our AchievementSet
                        continue;
                    }
                    
                    // Create the new Achievement
                    Achievement* ach = new Achievement();
                                        
                    // Plug in the properties to the new Achievement
                    ach->setAchievementFields(identifier, title, achievedDescription, unachieveDescription, maxPoints, hidden);
                    
                    // Register this Achievement so it can be accessed from TorqueScript
                    // and properly cleaned up during shut down
                    ach->registerObject(identifier);
                }
                
            }
            // Store the number of achievements in GameCenterManager, just in case
            achievementCount = [descriptions count];
            
            Con::printf("Achievements cached");
        }
     }];
}

@end


// The rest of the code consists of exposing the GameCenterManager to TorqueScript
// via global ConsoleFunctions

ConsoleFunction(isGameCenterAvailable, bool, 1, 1, "() Used to determine if current iOS is capable of using Game Center\n"
                                                   "@return True if Game Center is supported, false otherwise")
{
    return isGameCenterAvailable();
}

ConsoleFunction(initGameCenter, bool, 1, 1, "() Initialize Game Center and authenticate the player\n"
                                            "@return True if Game Center was successfully initialized, false if there was an error")
{
    if(isGameCenterAvailable())
    {
        gameCenterManager = [[GameCenterManager alloc] init];
        [gameCenterManager authenticateLocalUser];
    }
}

ConsoleFunction(closeGameCenter, void, 1, 1, "() This will cleanup and close Game Center. Should only be called when the game is closed")
{
    [gameCenterManager release];
}

ConsoleFunction(showLeaderboard, void, 1, 1, "() Display the leaderboards for this app using Game Center's view")
{
    if(isGameCenterAvailable())
    {
        [gameCenterManager showLeaderboard];
    }
    else
    {
        Con::printf("Failed to show leaderboard: Game Center is not supported");
    }
    
}

ConsoleFunction(showAchievements, void, 1, 1, "() Display the achievements for this app using Game Center's view")
{
    if(isGameCenterAvailable())
    {
        [gameCenterManager showAchievements];
    }
    else
    {
        Con::printf("Failed to show achievements: Game Center is not supported");
    }
}

ConsoleFunction(getAchievementCount, int, 1, 1, "() Gets the number of achievements this app supports from GameCenterManager\n"
                                                "@return Number of achievements obtained by connecting to iTunes Connect")
{
    return [gameCenterManager achievementCount];
}

ConsoleFunction(cacheAchievements, void, 1, 1, "() Forces achievement cacheing and populates the AchievementSet")
{
    [gameCenterManager cacheAchievements];
}

ConsoleFunction(submitScore, bool, 3, 3, "(int score, string category) Submit a score to a leaderboard, defined by category\n"
                                         "@param score Numerical value of the score being reported\n"
                                         "@param category String containing the unique category of the leaderboard\n"
                                         "@return True if the score was successfully submitted, false if there was an error")
{
    // Convert the identifier from a char* to a NSString
    NSString* category = [[[NSString alloc] initWithUTF8String:argv[2]] autorelease];
    
    // Convert score from char* to an int
    int score = dAtoi(argv[1]);
    
    // Call the GameCenterManager method for reporting a score
    [gameCenterManager reportScore:score forCategory:category];
}

ConsoleFunction(reportAchievement, bool, 3, 3, "(string identifier, float percentComplete) Reports achievement progress to Game Center\n"
                "@param identifier A unique string used to identify the achievement\n"
                "@param percentComplete Floating point value representing the progress of the achievement\n"
                "@return True if the achievement succesfully registered, false if there was an error")
{
    // Convert the identifier from a char* to a NSString
    NSString* identifier = [[[NSString alloc] initWithUTF8String:argv[1]] autorelease];
    
    // Convert the percentComplete from a char* to a float
    float percentComplete = dAtof(argv[2]);
    
    // Call the GameCenterManager method for reporting an achievement
    [gameCenterManager submitAchievement:identifier percentComplete:percentComplete];
}

ConsoleFunction(reloadHighScores, void, 2, 2, "(string category) Reload the highscore for a leaderboard, specified by category")
{
    NSString* category = [[[NSString alloc] initWithUTF8String:argv[1]] autorelease];
    
    [gameCenterManager reloadHighScoresForCategory:category];
}
ConsoleFunction(resetAchievements, bool, 1, 1, "() Reset the achievement progress for the authenticated player. This cannot be undone\n"
                                               "@return True if the achievements were successfully cleared, false if there was an error")
{
    [gameCenterManager resetAchievements];
}
#endif //TORQUE_ALLOW_GAMEKIT