<?php

namespace OAuth2\Storage;

class Bootstrap
{
    const DYNAMODB_PHP_VERSION = 'none';

    protected static $instance;
    private $mysql;
    private $sqlite;
    private $postgres;
    private $mongo;
    private $mongoDb;
    private $redis;
    private $cassandra;
    private $configDir;
    private $dynamodb;
    private $couchbase;

    public function __construct()
    {
        $this->configDir = __DIR__.'/../../../config';
    }

    public static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public function getSqlitePdo()
    {
        if (!$this->sqlite) {
            $this->removeSqliteDb();
            $pdo = new \PDO(sprintf('sqlite:%s', $this->getSqliteDir()));
            $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
            $this->createSqliteDb($pdo);

            $this->sqlite = new Pdo($pdo);
        }

        return $this->sqlite;
    }

    public function getPostgresPdo()
    {
        if (!$this->postgres) {
            if (in_array('pgsql', \PDO::getAvailableDrivers())) {
                $this->removePostgresDb();
                $this->createPostgresDb();
                if ($pdo = $this->getPostgresDriver()) {
                    $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
                    $this->populatePostgresDb($pdo);
                    $this->postgres = new Pdo($pdo);
                }
            } else {
                $this->postgres = new NullStorage('Postgres', 'Missing postgres PDO extension.');
            }
        }

        return $this->postgres;
    }

    public function getPostgresDriver()
    {
        try {
            $pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres');

            return $pdo;
        } catch (\PDOException $e) {
            $this->postgres = new NullStorage('Postgres', $e->getMessage());
        }
    }

    public function getMemoryStorage()
    {
        return new Memory(json_decode(file_get_contents($this->configDir. '/storage.json'), true));
    }

    public function getRedisStorage()
    {
        if (!$this->redis) {
            if (class_exists('Predis\Client')) {
                $redis = new \Predis\Client();
                if ($this->testRedisConnection($redis)) {
                    $redis->flushdb();
                    $this->redis = new Redis($redis);
                    $this->createRedisDb($this->redis);
                } else {
                    $this->redis = new NullStorage('Redis', 'Unable to connect to redis server on port 6379');
                }
            } else {
                $this->redis = new NullStorage('Redis', 'Missing redis library. Please run "composer.phar require predis/predis:dev-master"');
            }
        }

        return $this->redis;
    }

    private function testRedisConnection(\Predis\Client $redis)
    {
        try {
            $redis->connect();
        } catch (\Predis\CommunicationException $exception) {
            // we were unable to connect to the redis server
            return false;
        }

        return true;
    }

    public function getMysqlPdo()
    {
        if (!$this->mysql) {
            $pdo = null;
            try {
                $pdo = new \PDO('mysql:host=localhost;', 'root');
            } catch (\PDOException $e) {
                $this->mysql = new NullStorage('MySQL', 'Unable to connect to MySQL on root@localhost');
            }

            if ($pdo) {
                $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
                $this->removeMysqlDb($pdo);
                $this->createMysqlDb($pdo);

                $this->mysql = new Pdo($pdo);
            }
        }

        return $this->mysql;
    }

    public function getMongo()
    {
        if (!$this->mongo) {
            if (class_exists('MongoClient')) {
                $mongo = new \MongoClient('mongodb://localhost:27017', array('connect' => false));
                if ($this->testMongoConnection($mongo)) {
                    $db = $mongo->oauth2_server_php_legacy;
                    $this->removeMongo($db);
                    $this->createMongo($db);

                    $this->mongo = new Mongo($db);
                } else {
                    $this->mongo = new NullStorage('Mongo', 'Unable to connect to mongo server on "localhost:27017"');
                }
            } else {
                $this->mongo = new NullStorage('Mongo', 'Missing mongo php extension. Please install mongo.so');
            }
        }

        return $this->mongo;
    }

    public function getMongoDb()
    {
        if (!$this->mongoDb) {
            if (class_exists('MongoDB\Client')) {
                $mongoDb = new \MongoDB\Client('mongodb://localhost:27017');
                if ($this->testMongoDBConnection($mongoDb)) {
                    $db = $mongoDb->oauth2_server_php;
                    $this->removeMongoDb($db);
                    $this->createMongoDb($db);

                    $this->mongoDb = new MongoDB($db);
                } else {
                    $this->mongoDb = new NullStorage('MongoDB', 'Unable to connect to mongo server on "localhost:27017"');
                }
            } else {
                $this->mongoDb = new NullStorage('MongoDB', 'Missing MongoDB php extension. Please install mongodb.so');
            }
        }

        return $this->mongoDb;
    }

    private function testMongoConnection(\MongoClient $mongo)
    {
        try {
            $mongo->connect();
        } catch (\MongoConnectionException $e) {
            return false;
        }

        return true;
    }

    private function testMongoDBConnection(\MongoDB\Client $mongo)
    {
        return true;
    }

    public function getCouchbase()
    {
        if (!$this->couchbase) {
            if ($this->getEnvVar('SKIP_COUCHBASE_TESTS')) {
                $this->couchbase = new NullStorage('Couchbase', 'Skipping Couchbase tests');
            } elseif (!class_exists('Couchbase')) {
                $this->couchbase = new NullStorage('Couchbase', 'Missing Couchbase php extension. Please install couchbase.so');
            } else {
                // round-about way to make sure couchbase is working
                // this is required because it throws a "floating point exception" otherwise
                $code = "new \Couchbase(array('localhost:8091'), '', '', 'auth', false);";
                $exec = sprintf('php -r "%s"', $code);
                $ret = exec($exec, $test, $var);
                if ($ret != 0) {
                    $couchbase = new \Couchbase(array('localhost:8091'), '', '', 'auth', false);
                    if ($this->testCouchbaseConnection($couchbase)) {
                        $this->clearCouchbase($couchbase);
                        $this->createCouchbaseDB($couchbase);

                        $this->couchbase = new CouchbaseDB($couchbase);
                    } else {
                        $this->couchbase = new NullStorage('Couchbase', 'Unable to connect to Couchbase server on "localhost:8091"');
                    }
                } else {
                    $this->couchbase = new NullStorage('Couchbase', 'Error while trying to connect to Couchbase');
                }
            }
        }

        return $this->couchbase;
    }

    private function testCouchbaseConnection(\Couchbase $couchbase)
    {
        try {
            if (count($couchbase->getServers()) > 0) {
                return true;
            }
        } catch (\CouchbaseException $e) {
            return false;
        }

        return true;
    }

    public function getCassandraStorage()
    {
        if (!$this->cassandra) {
            if (class_exists('phpcassa\ColumnFamily')) {
                $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160'));
                if ($this->testCassandraConnection($cassandra)) {
                    $this->removeCassandraDb();
                    $this->cassandra = new Cassandra($cassandra);
                    $this->createCassandraDb($this->cassandra);
                } else {
                    $this->cassandra = new NullStorage('Cassandra', 'Unable to connect to cassandra server on "127.0.0.1:9160"');
                }
            } else {
                $this->cassandra = new NullStorage('Cassandra', 'Missing cassandra library. Please run "composer.phar require thobbs/phpcassa:dev-master"');
            }
        }

        return $this->cassandra;
    }

    private function testCassandraConnection(\phpcassa\Connection\ConnectionPool $cassandra)
    {
        try {
            new \phpcassa\SystemManager('localhost:9160');
        } catch (\Exception $e) {
            return false;
        }

        return true;
    }

    private function removeCassandraDb()
    {
        $sys = new \phpcassa\SystemManager('localhost:9160');

        try {
            $sys->drop_keyspace('oauth2_test');
        } catch (\cassandra\InvalidRequestException $e) {

        }
    }

    private function createCassandraDb(Cassandra $storage)
    {
        // create the cassandra keyspace and column family
        $sys = new \phpcassa\SystemManager('localhost:9160');

        $sys->create_keyspace('oauth2_test', array(
            "strategy_class" => \phpcassa\Schema\StrategyClass::SIMPLE_STRATEGY,
            "strategy_options" => array('replication_factor' => '1')
        ));

        $sys->create_column_family('oauth2_test', 'auth');
        $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160'));
        $cf = new \phpcassa\ColumnFamily($cassandra, 'auth');

        // populate the data
        $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password');
        $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000);
        $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000);

        $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4');
        $storage->setScope('defaultscope1 defaultscope2', null, 'default');

        $storage->setScope('clientscope1 clientscope2', 'Test Client ID');
        $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default');

        $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2');
        $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default');

        $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID');
        $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default');

        $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2');
        $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default');

        $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject');

        $cf->insert("oauth_public_keys:ClientID_One", array('__data' => json_encode(array("public_key" => "client_1_public", "private_key" => "client_1_private", "encryption_algorithm" => "RS256"))));
        $cf->insert("oauth_public_keys:ClientID_Two", array('__data' => json_encode(array("public_key" => "client_2_public", "private_key" => "client_2_private", "encryption_algorithm" => "RS256"))));
        $cf->insert("oauth_public_keys:", array('__data' => json_encode(array("public_key" => $this->getTestPublicKey(), "private_key" =>  $this->getTestPrivateKey(), "encryption_algorithm" => "RS256"))));

        $cf->insert("oauth_users:testuser", array('__data' =>json_encode(array("password" => "password", "email" => "testuser@test.com", "email_verified" => true))));

    }

    private function createSqliteDb(\PDO $pdo)
    {
        $this->runPdoSql($pdo);
    }

    private function removeSqliteDb()
    {
        if (file_exists($this->getSqliteDir())) {
            unlink($this->getSqliteDir());
        }
    }

    private function createMysqlDb(\PDO $pdo)
    {
        $pdo->exec('CREATE DATABASE oauth2_server_php');
        $pdo->exec('USE oauth2_server_php');
        $this->runPdoSql($pdo);
    }

    private function removeMysqlDb(\PDO $pdo)
    {
        $pdo->exec('DROP DATABASE IF EXISTS oauth2_server_php');
    }

    private function createPostgresDb()
    {
        if (!`psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='postgres'"`) {
            `createuser -s -r postgres`;
        }

        `createdb -O postgres oauth2_server_php`;
    }

    private function populatePostgresDb(\PDO $pdo)
    {
        $this->runPdoSql($pdo);
    }

    private function removePostgresDb()
    {
        if (trim(`psql -l | grep oauth2_server_php | wc -l`)) {
            `dropdb oauth2_server_php`;
        }
    }

    public function runPdoSql(\PDO $pdo)
    {
        $storage = new Pdo($pdo);
        foreach (explode(';', $storage->getBuildSql()) as $statement) {
            $result = $pdo->exec($statement);
        }

        // set up scopes
        $sql = 'INSERT INTO oauth_scopes (scope) VALUES (?)';
        foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) {
            $pdo->prepare($sql)->execute(array($supportedScope));
        }

        $sql = 'INSERT INTO oauth_scopes (scope, is_default) VALUES (?, ?)';
        foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) {
            $pdo->prepare($sql)->execute(array($defaultScope, true));
        }

        // set up clients
        $sql = 'INSERT INTO oauth_clients (client_id, client_secret, scope, grant_types) VALUES (?, ?, ?, ?)';
        $pdo->prepare($sql)->execute(array('Test Client ID', 'TestSecret', 'clientscope1 clientscope2', null));
        $pdo->prepare($sql)->execute(array('Test Client ID 2', 'TestSecret', 'clientscope1 clientscope2 clientscope3', null));
        $pdo->prepare($sql)->execute(array('Test Default Scope Client ID', 'TestSecret', 'clientscope1 clientscope2', null));
        $pdo->prepare($sql)->execute(array('oauth_test_client', 'testpass', null, 'implicit password'));

        // set up misc
        $sql = 'INSERT INTO oauth_access_tokens (access_token, client_id, expires, user_id) VALUES (?, ?, ?, ?)';
        $pdo->prepare($sql)->execute(array('testtoken', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), null));
        $pdo->prepare($sql)->execute(array('accesstoken-openid-connect', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour')), 'testuser'));

        $sql = 'INSERT INTO oauth_authorization_codes (authorization_code, client_id, expires) VALUES (?, ?, ?)';
        $pdo->prepare($sql)->execute(array('testcode', 'Some Client', date('Y-m-d H:i:s', strtotime('+1 hour'))));

        $sql = 'INSERT INTO oauth_users (username, password, email, email_verified) VALUES (?, ?, ?, ?)';
        $pdo->prepare($sql)->execute(array('testuser', 'password', 'testuser@test.com', true));

        $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)';
        $pdo->prepare($sql)->execute(array('ClientID_One', 'client_1_public', 'client_1_private', 'RS256'));
        $pdo->prepare($sql)->execute(array('ClientID_Two', 'client_2_public', 'client_2_private', 'RS256'));

        $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)';
        $pdo->prepare($sql)->execute(array(null, $this->getTestPublicKey(), $this->getTestPrivateKey(), 'RS256'));

        $sql = 'INSERT INTO oauth_jwt (client_id, subject, public_key) VALUES (?, ?, ?)';
        $pdo->prepare($sql)->execute(array('oauth_test_client', 'test_subject', $this->getTestPublicKey()));
    }

    public function getSqliteDir()
    {
        return $this->configDir. '/test.sqlite';
    }

    public function getConfigDir()
    {
        return $this->configDir;
    }

    private function createCouchbaseDB(\Couchbase $db)
    {
        $db->set('oauth_clients-oauth_test_client',json_encode(array(
            'client_id' => "oauth_test_client",
            'client_secret' => "testpass",
            'redirect_uri' => "http://example.com",
            'grant_types' => 'implicit password'
        )));

        $db->set('oauth_access_tokens-testtoken',json_encode(array(
            'access_token' => "testtoken",
            'client_id' => "Some Client"
        )));

        $db->set('oauth_authorization_codes-testcode',json_encode(array(
            'access_token' => "testcode",
            'client_id' => "Some Client"
        )));

        $db->set('oauth_users-testuser',json_encode(array(
            'username' => 'testuser',
            'password' => 'password',
            'email' => 'testuser@test.com',
            'email_verified' => true,
        )));

        $db->set('oauth_jwt-oauth_test_client',json_encode(array(
            'client_id' => 'oauth_test_client',
            'key'       => $this->getTestPublicKey(),
            'subject'   => 'test_subject',
        )));
    }

    private function clearCouchbase(\Couchbase $cb)
    {
        $cb->delete('oauth_authorization_codes-new-openid-code');
        $cb->delete('oauth_access_tokens-newtoken');
        $cb->delete('oauth_authorization_codes-newcode');
        $cb->delete('oauth_refresh_tokens-refreshtoken');
    }

    private function createMongo(\MongoDB $db)
    {
        $db->oauth_clients->insert(array(
            'client_id' => "oauth_test_client",
            'client_secret' => "testpass",
            'redirect_uri' => "http://example.com",
            'grant_types' => 'implicit password'
        ));

        $db->oauth_access_tokens->insert(array(
            'access_token' => "testtoken",
            'client_id' => "Some Client"
        ));

        $db->oauth_authorization_codes->insert(array(
            'authorization_code' => "testcode",
            'client_id' => "Some Client"
        ));

        $db->oauth_users->insert(array(
            'username' => 'testuser',
            'password' => 'password',
            'email' => 'testuser@test.com',
            'email_verified' => true,
        ));

        $db->oauth_keys->insert(array(
            'client_id'   => null,
            'public_key' => $this->getTestPublicKey(),
            'private_key' => $this->getTestPrivateKey(),
            'encryption_algorithm' => 'RS256'
        ));

        $db->oauth_jwt->insert(array(
            'client_id' => 'oauth_test_client',
            'key' => $this->getTestPublicKey(),
            'subject'   => 'test_subject',
        ));
    }

    public function removeMongo(\MongoDB $db)
    {
        $db->drop();
    }

    private function createMongoDB(\MongoDB\Database $db)
    {
        $db->oauth_clients->insertOne(array(
            'client_id' => "oauth_test_client",
            'client_secret' => "testpass",
            'redirect_uri' => "http://example.com",
            'grant_types' => 'implicit password'
        ));

        $db->oauth_access_tokens->insertOne(array(
            'access_token' => "testtoken",
            'client_id' => "Some Client"
        ));

        $db->oauth_authorization_codes->insertOne(array(
            'authorization_code' => "testcode",
            'client_id' => "Some Client"
        ));

        $db->oauth_users->insertOne(array(
            'username' => 'testuser',
            'password' => 'password',
            'email' => 'testuser@test.com',
            'email_verified' => true,
        ));

        $db->oauth_keys->insertOne(array(
            'client_id'   => null,
            'public_key' => $this->getTestPublicKey(),
            'private_key' => $this->getTestPrivateKey(),
            'encryption_algorithm' => 'RS256'
        ));

        $db->oauth_jwt->insertOne(array(
            'client_id' => 'oauth_test_client',
            'key' => $this->getTestPublicKey(),
            'subject'   => 'test_subject',
        ));
    }

    public function removeMongoDB(\MongoDB\Database $db)
    {
        $db->drop();
    }

    private function createRedisDb(Redis $storage)
    {
        $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password');
        $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000);
        $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000);
        $storage->setUser("testuser", "password");

        $storage->setScope('supportedscope1 supportedscope2 supportedscope3 supportedscope4');
        $storage->setScope('defaultscope1 defaultscope2', null, 'default');

        $storage->setScope('clientscope1 clientscope2', 'Test Client ID');
        $storage->setScope('clientscope1 clientscope2', 'Test Client ID', 'default');

        $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Client ID 2');
        $storage->setScope('clientscope1 clientscope2', 'Test Client ID 2', 'default');

        $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID');
        $storage->setScope('clientscope1 clientscope2', 'Test Default Scope Client ID', 'default');

        $storage->setScope('clientscope1 clientscope2 clientscope3', 'Test Default Scope Client ID 2');
        $storage->setScope('clientscope3', 'Test Default Scope Client ID 2', 'default');

        $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject');
    }

    public function getTestPublicKey()
    {
        return file_get_contents(__DIR__.'/../../../config/keys/id_rsa.pub');
    }

    private function getTestPrivateKey()
    {
        return file_get_contents(__DIR__.'/../../../config/keys/id_rsa');
    }

    public function getDynamoDbStorage()
    {
        if (!$this->dynamodb) {
            // only run once per travis build
            if (true == $this->getEnvVar('TRAVIS')) {
                if (self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) {
                    $this->dynamodb = new NullStorage('DynamoDb', 'Skipping for travis.ci - only run once per build');

                    return;
                }
            }
            if (class_exists('\Aws\DynamoDb\DynamoDbClient')) {
                if ($client = $this->getDynamoDbClient()) {
                    // travis runs a unique set of tables per build, to avoid conflict
                    $prefix = '';
                    if ($build_id = $this->getEnvVar('TRAVIS_JOB_NUMBER')) {
                        $prefix = sprintf('build_%s_', $build_id);
                    } else {
                        if (!$this->deleteDynamoDb($client, $prefix, true)) {
                            return $this->dynamodb = new NullStorage('DynamoDb', 'Timed out while waiting for DynamoDB deletion (30 seconds)');
                        }
                    }
                    $this->createDynamoDb($client, $prefix);
                    $this->populateDynamoDb($client, $prefix);
                    $config = array(
                        'client_table' => $prefix.'oauth_clients',
                        'access_token_table' => $prefix.'oauth_access_tokens',
                        'refresh_token_table' => $prefix.'oauth_refresh_tokens',
                        'code_table' => $prefix.'oauth_authorization_codes',
                        'user_table' => $prefix.'oauth_users',
                        'jwt_table'  => $prefix.'oauth_jwt',
                        'scope_table'  => $prefix.'oauth_scopes',
                        'public_key_table'  => $prefix.'oauth_public_keys',
                    );
                    $this->dynamodb = new DynamoDB($client, $config);
                } elseif (!$this->dynamodb) {
                    $this->dynamodb = new NullStorage('DynamoDb', 'unable to connect to DynamoDB');
                }
            } else {
                $this->dynamodb = new NullStorage('DynamoDb', 'Missing DynamoDB library. Please run "composer.phar require aws/aws-sdk-php:dev-master');
            }
        }

        return $this->dynamodb;
    }

    private function getDynamoDbClient()
    {
        $config = array();
        // check for environment variables
        if (($key = $this->getEnvVar('AWS_ACCESS_KEY_ID')) && ($secret = $this->getEnvVar('AWS_SECRET_KEY'))) {
            $config['key']    = $key;
            $config['secret'] = $secret;
        } else {
            // fall back on ~/.aws/credentials file
            // @see http://docs.aws.amazon.com/aws-sdk-php/guide/latest/credentials.html#credential-profiles
            if (!file_exists($this->getEnvVar('HOME') . '/.aws/credentials')) {
                $this->dynamodb = new NullStorage('DynamoDb', 'No aws credentials file found, and no AWS_ACCESS_KEY_ID or AWS_SECRET_KEY environment variable set');

                return;
            }

            // set profile in AWS_PROFILE environment variable, defaults to "default"
            $config['profile'] = $this->getEnvVar('AWS_PROFILE', 'default');
        }

        // set region in AWS_REGION environment variable, defaults to "us-east-1"
        $config['region'] = $this->getEnvVar('AWS_REGION', \Aws\Common\Enum\Region::US_EAST_1);

        return \Aws\DynamoDb\DynamoDbClient::factory($config);
    }

    private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null, $waitForDeletion = false)
    {
        $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users');
        $nbTables  = count($tablesList);

        // Delete all table.
        foreach ($tablesList as $key => $table) {
            try {
                $client->deleteTable(array('TableName' => $prefix.$table));
            } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) {
                // Table does not exist : nothing to do
            }
        }

        // Wait for deleting
        if ($waitForDeletion) {
            $retries = 5;
            $nbTableDeleted = 0;
            while ($nbTableDeleted != $nbTables) {
                $nbTableDeleted = 0;
                foreach ($tablesList as $key => $table) {
                    try {
                        $result = $client->describeTable(array('TableName' => $prefix.$table));
                    } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) {
                        // Table does not exist : nothing to do
                        $nbTableDeleted++;
                    }
                }
                if ($nbTableDeleted != $nbTables) {
                    if ($retries < 0) {
                        // we are tired of waiting
                        return false;
                    }
                    sleep(5);
                    echo "Sleeping 5 seconds for DynamoDB ($retries more retries)...\n";
                    $retries--;
                }
            }
        }

        return true;
    }

    private function createDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null)
    {
        $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users');
        $nbTables  = count($tablesList);
        $client->createTable(array(
            'TableName' => $prefix.'oauth_access_tokens',
            'AttributeDefinitions' => array(
                array('AttributeName' => 'access_token','AttributeType' => 'S')
            ),
            'KeySchema' => array(array('AttributeName' => 'access_token','KeyType' => 'HASH')),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        $client->createTable(array(
            'TableName' => $prefix.'oauth_authorization_codes',
            'AttributeDefinitions' => array(
                array('AttributeName' => 'authorization_code','AttributeType' => 'S')
            ),
            'KeySchema' => array(array('AttributeName' => 'authorization_code','KeyType' => 'HASH')),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        $client->createTable(array(
            'TableName' => $prefix.'oauth_clients',
            'AttributeDefinitions' => array(
                array('AttributeName' => 'client_id','AttributeType' => 'S')
            ),
            'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        $client->createTable(array(
            'TableName' => $prefix.'oauth_jwt',
            'AttributeDefinitions' => array(
                array('AttributeName' => 'client_id','AttributeType' => 'S'),
                array('AttributeName' => 'subject','AttributeType' => 'S')
            ),
            'KeySchema' => array(
                array('AttributeName' => 'client_id','KeyType' => 'HASH'),
                array('AttributeName' => 'subject','KeyType' => 'RANGE')
            ),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        $client->createTable(array(
            'TableName' => $prefix.'oauth_public_keys',
            'AttributeDefinitions' => array(
                array('AttributeName' => 'client_id','AttributeType' => 'S')
            ),
            'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        $client->createTable(array(
            'TableName' => $prefix.'oauth_refresh_tokens',
            'AttributeDefinitions' => array(
                array('AttributeName' => 'refresh_token','AttributeType' => 'S')
            ),
            'KeySchema' => array(array('AttributeName' => 'refresh_token','KeyType' => 'HASH')),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        $client->createTable(array(
            'TableName' => $prefix.'oauth_scopes',
            'AttributeDefinitions' => array(
                array('AttributeName' => 'scope','AttributeType' => 'S'),
                array('AttributeName' => 'is_default','AttributeType' => 'S')
            ),
            'KeySchema' => array(array('AttributeName' => 'scope','KeyType' => 'HASH')),
            'GlobalSecondaryIndexes' => array(
                array(
                    'IndexName' => 'is_default-index',
                    'KeySchema' => array(array('AttributeName' => 'is_default', 'KeyType' => 'HASH')),
                    'Projection' => array('ProjectionType' => 'ALL'),
                    'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
                ),
            ),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        $client->createTable(array(
            'TableName' => $prefix.'oauth_users',
            'AttributeDefinitions' => array(array('AttributeName' => 'username','AttributeType' => 'S')),
            'KeySchema' => array(array('AttributeName' => 'username','KeyType' => 'HASH')),
            'ProvisionedThroughput' => array('ReadCapacityUnits'  => 1,'WriteCapacityUnits' => 1)
        ));

        // Wait for creation
        $nbTableCreated = 0;
        while ($nbTableCreated != $nbTables) {
            $nbTableCreated = 0;
            foreach ($tablesList as $key => $table) {
                try {
                    $result = $client->describeTable(array('TableName' => $prefix.$table));
                    if ($result['Table']['TableStatus'] == 'ACTIVE') {
                        $nbTableCreated++;
                    }
                } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) {
                    // Table does not exist : nothing to do
                    $nbTableCreated++;
                }
            }
            if ($nbTableCreated != $nbTables) {
                sleep(1);
            }
        }
    }

    private function populateDynamoDb($client, $prefix = null)
    {
        // set up scopes
        foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) {
            $client->putItem(array(
                'TableName' => $prefix.'oauth_scopes',
                'Item' => array('scope' => array('S' => $supportedScope))
            ));
        }

        foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) {
            $client->putItem(array(
                'TableName' => $prefix.'oauth_scopes',
                'Item' => array('scope' => array('S' => $defaultScope), 'is_default' => array('S' => "true"))
            ));
        }

        $client->putItem(array(
            'TableName' => $prefix.'oauth_clients',
            'Item' => array(
                'client_id' => array('S' => 'Test Client ID'),
                'client_secret' => array('S' => 'TestSecret'),
                'scope' => array('S' => 'clientscope1 clientscope2')
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_clients',
            'Item' => array(
                'client_id' => array('S' => 'Test Client ID 2'),
                'client_secret' => array('S' => 'TestSecret'),
                'scope' => array('S' => 'clientscope1 clientscope2 clientscope3')
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_clients',
            'Item' => array(
                'client_id' => array('S' => 'Test Default Scope Client ID'),
                'client_secret' => array('S' => 'TestSecret'),
                'scope' => array('S' => 'clientscope1 clientscope2')
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_clients',
            'Item' => array(
                'client_id' => array('S' => 'oauth_test_client'),
                'client_secret' => array('S' => 'testpass'),
                'grant_types' => array('S' => 'implicit password')
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_access_tokens',
            'Item' => array(
                'access_token' => array('S' => 'testtoken'),
                'client_id' => array('S' => 'Some Client'),
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_access_tokens',
            'Item' => array(
                 'access_token' => array('S' => 'accesstoken-openid-connect'),
                 'client_id' => array('S' => 'Some Client'),
                 'user_id' => array('S' => 'testuser'),
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_authorization_codes',
            'Item' => array(
                'authorization_code' => array('S' => 'testcode'),
                'client_id' => array('S' => 'Some Client'),
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_users',
            'Item' => array(
                'username' => array('S' => 'testuser'),
                'password' => array('S' => 'password'),
                'email' => array('S' => 'testuser@test.com'),
                'email_verified' => array('S' => 'true'),
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_public_keys',
            'Item' => array(
                'client_id' => array('S' => 'ClientID_One'),
                'public_key' => array('S' => 'client_1_public'),
                'private_key' => array('S' => 'client_1_private'),
                'encryption_algorithm' => array('S' => 'RS256'),
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_public_keys',
            'Item' => array(
                'client_id' => array('S' => 'ClientID_Two'),
                'public_key' => array('S' => 'client_2_public'),
                'private_key' => array('S' => 'client_2_private'),
                'encryption_algorithm' => array('S' => 'RS256'),
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_public_keys',
            'Item' => array(
                'client_id' => array('S' => '0'),
                'public_key' => array('S' => $this->getTestPublicKey()),
                'private_key' => array('S' => $this->getTestPrivateKey()),
                'encryption_algorithm' => array('S' => 'RS256'),
            )
        ));

        $client->putItem(array(
            'TableName' => $prefix.'oauth_jwt',
            'Item' => array(
                'client_id' => array('S' => 'oauth_test_client'),
                'subject' => array('S' => 'test_subject'),
                'public_key' => array('S' => $this->getTestPublicKey()),
            )
        ));
    }

    public function cleanupTravisDynamoDb($prefix = null)
    {
        if (is_null($prefix)) {
            // skip this when not applicable
            if (!$this->getEnvVar('TRAVIS') || self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) {
                return;
            }

            $prefix = sprintf('build_%s_', $this->getEnvVar('TRAVIS_JOB_NUMBER'));
        }

        $client = $this->getDynamoDbClient();
        $this->deleteDynamoDb($client, $prefix);
    }

    private function getEnvVar($var, $default = null)
    {
        return isset($_SERVER[$var]) ? $_SERVER[$var] : (getenv($var) ?: $default);
    }
}
