package util.game;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import external.JSON.JSONObject;

import util.configuration.ProjectConfiguration;

/**
 * Cloud game repositories provide access to game resources stored on game
 * repository servers on the web, while continuing to work while the user is
 * offline through aggressive caching based on the immutability + versioning
 * scheme provided by the repository servers.
 * 
 * Essentially, each game has a version number stored in the game metadata
 * file. Game resources are immutable until this version number changes, at
 * which point the game needs to be reloaded. Version numbers are passed along
 * and stored in the match descriptions, and repository servers will continue
 * to serve old versions when specifically requested, so it is valid to use any
 * historical game version when generating a match -- this is why we don't need
 * to worry about our offline cache becoming stale/invalid. However, to stay up
 * to date with the latest bugfixes, etc, we aggressively refresh the cache any
 * time we can connect to the repository server, as a matter of policy.
 * 
 * Cached games are stored locally, in a directory managed by this class. These
 * files are compressed, to decrease their footprint on the local disk. GGP Base
 * has its SVN rules set up so that these caches are ignored by SVN. 
 * 
 * @author Sam
 */
public final class CloudGameRepository extends GameRepository {
    private final String theRepoURL;
    private final File theCacheDirectory;
    
    public CloudGameRepository(String theURL) {
        if (!theURL.startsWith("http://"))
            theURL = "http://" + theURL;
        if (!theURL.endsWith("/"))
            theURL = theURL + "/";
        theRepoURL = theURL;
        
        // Generate a unique hash of the repository URL, to use as the
        // local directory for files for the offline cache.
        StringBuilder theCacheHash = new StringBuilder();
        try {
            byte[] bytesOfMessage = theRepoURL.getBytes("UTF-8");
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] theDigest = md.digest(bytesOfMessage);            
            for(int i = 0; i < theDigest.length; i++) {
                theCacheHash.append(Math.abs(theDigest[i]));
            }            
        } catch(Exception e) {
            theCacheHash = null;
        }
        
        theCacheDirectory = new File(ProjectConfiguration.gameCacheDirectory, "repoHash" + theCacheHash);
        theCacheDirectory.mkdir();
        attemptToRefreshOfflineCache();
    }
    
    protected Set<String> getUncachedGameKeys() {
        Set<String> theKeys = new HashSet<String>();
        for(File game : theCacheDirectory.listFiles()) {
            theKeys.add(game.getName().replace(".zip", ""));
        }
        return theKeys;
    }
    
    protected Game getUncachedGame(String theKey) {
        return loadGameFromCache(theKey);
    }

    // ================================================================
    
    // Games are cached asynchronously in their own threads.
    class RefreshCacheForGameThread extends Thread {
        RemoteGameRepository theRepository;
        String theKey;
        
        public RefreshCacheForGameThread(RemoteGameRepository a, String b) {
            theRepository = a;
            theKey = b;
        }
        
        @Override
        public void run() {
            try {
                String theGameURL = theRepository.getGameURL(theKey);
                JSONObject theMetadata = RemoteGameRepository.getGameMetadataFromRepository(theGameURL);

                int repoVersion = theMetadata.getInt("version");
                String versionedRepoURL = RemoteGameRepository.addVersionToGameURL(theGameURL, repoVersion);

                Game myGameVersion = loadGameFromCache(theKey);
                String myVersionedRepoURL = "";
                if (myGameVersion != null)
                    myVersionedRepoURL = myGameVersion.getRepositoryURL();

                if (!versionedRepoURL.equals(myVersionedRepoURL)) {
                    // Cache miss: we don't have the current version for
                    // this game, and so we need to load it from the web.
                    Game theGame = RemoteGameRepository.loadSingleGameFromMetadata(theKey, theGameURL, theMetadata);
                    saveGameToCache(theKey, theGame);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    private void attemptToRefreshOfflineCache() {
        RemoteGameRepository remoteRepository = new RemoteGameRepository(theRepoURL);

        System.out.println("Updating the game cache...");
        long beginTime = System.currentTimeMillis();
        
        // Since games are immutable, we can guarantee that the games listed
        // by the repository server includes the games in the local cache, so
        // we can be happy just updating/refreshing the listed games.
        Set<String> theGameKeys = remoteRepository.getGameKeys();
        if (theGameKeys == null) return;
        
        // Start threads to update every entry in the cache (or at least verify
        // that the entry doesn't need to be updated).
        Set<Thread> theThreads = new HashSet<Thread>();
        for (String gameKey : theGameKeys) {
            Thread t = new RefreshCacheForGameThread(remoteRepository, gameKey);
            t.start();
            theThreads.add(t);
        }
        
        // Wait until we've updated the cache before continuing.
        for (Thread t : theThreads) {
            try {
                t.join();
            } catch (Exception e) {
                ;
            }
        }
        
        long endTime = System.currentTimeMillis();
        System.out.println("Updating the game cache took: " + (endTime - beginTime) + "ms.");
    }
    
    // ================================================================    

    private void saveGameToCache(String theKey, Game theGame) {
        File theGameFile = new File(theCacheDirectory, theKey + ".zip");
        try {
            theGameFile.createNewFile();
            FileOutputStream fOut = new FileOutputStream(theGameFile);
            GZIPOutputStream gOut = new GZIPOutputStream(fOut);
            PrintWriter pw = new PrintWriter(gOut);
            pw.print(theGame.serializeToJSON());
            pw.flush();
            pw.close();
            gOut.close();
            fOut.close();
        } catch (Exception e) {
            e.printStackTrace();            
        }
    }
    
    private Game loadGameFromCache(String theKey) {
        File theGameFile = new File(theCacheDirectory, theKey + ".zip");        
        String theLine = null;
        try {
            FileInputStream fIn = new FileInputStream(theGameFile);
            GZIPInputStream gIn = new GZIPInputStream(fIn);
            InputStreamReader ir = new InputStreamReader(gIn);
            BufferedReader br = new BufferedReader(ir);
            theLine = br.readLine();
            br.close();
            ir.close();
            gIn.close();
            fIn.close();
        } catch (Exception e) {
            ;
        }
        
        if (theLine == null) return null;
        return Game.loadFromJSON(theLine);
    }

    // ================================================================
    
    public static void main(String[] args) {
        GameRepository theRepository = new CloudGameRepository("http://ggp-repository.appspot.com");

        long beginTime = System.currentTimeMillis();

        Map<String, Game> theGames = new HashMap<String, Game>();
        for(String gameKey : theRepository.getGameKeys()) {
            theGames.put(gameKey, theRepository.getGame(gameKey));            
        }
        System.out.println("Games: " + theGames.size());
        
        long endTime = System.currentTimeMillis();
        System.out.println("Time: " + (endTime - beginTime) + "ms.");
    }
}
@@ -2063,25 +2063,24 @@
         if (
-!
 theURL.endsW
@@ -2121,14 +2121,40 @@
 eURL
- + %22/%22
+.substring(0, theURL.length()-1)
 ;%0D%0A 

Clean up how we generate repository URLs.