// Contract: PrivateBet
// Target function: function donate(uint _id) public payable
.Open, "ImproperState");    // Etat 'ouvert' requis
        
        // vérif signature de web3.eth.accounts.sign({participant})
        bytes32 msgHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n20", msg.sender));
        address signer = ecrecover(msgHash, _vSign, _rSign, _sSign);
        require(signer != address(0), "InvalidSignature"); // signature invalide (bug)
        require(signer == bt.guestPubKeyAdr, "WrongSigner");  // signataire incorrecte (bug, invitation modifiée/désactivée ou tentative de fraude)
        
        // peut-on encore en ajouter un ?
        if (bt.nbParticipant < NB_MAX_PARTICIPANT) {
            address p = msg.sender;
            
            // on ajoute uniquement le participant s'il n'est pas déjà connu
            if (!bt.participants[p].knownUser) {
                bt.participants[p].knownUser = true;

                // prise en compte du participant
                bt.nbParticipant++;
                emit NewParticiant(_id, p);
            }
        }
    }

    /// @notice Permet à l'utilisateur de savoir s'il particpe à un pari donné
    /// @dev    Seul moyen de connaître l'info pour les tests unitaires
    /// @param  _id identifiant du pari
    /// @return true si l'utilisateur fait parti des participants, false sinon
    function isParticipant(uint _id) public view returns(bool)  {
        return m_bets[_id].participants[msg.sender].knownUser;
    }

    /// @notice Permet à *l'organisateur* de changer la pseudo clé publique d'invitation dans le cas où la clé privée
    ///         correspondante est compromise ou pour suspendre les invitations.
    /// @param  _id identifiant du pari
    /// @param _pubKeyAdr nouvelle pseudo clé publique à utiliser. 0x0 désactive `participate()`
    function setGuestPubKeyAdr(uint _id, address _pubKeyAdr) public onlyOrganizer(_id) {
        m_bets[_id].guestPubKeyAdr = _pubKeyAdr;
    }

    /// @notice Permet à *l'organisateur* d'annuler le pari, uniquement lorsque le pari est dans l'état ouvert.
    /// @dev le pari passe dans l'état 'annulé'. Emmet l'évenement `BetCanceled` pour enrichir l'historique.
    /// @param  _id identifiant du pari
    function cancelBet(uint _id) public onlyOrganizer(_id) {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.Open, "ImproperState"); // Etat 'ouvert' requis

        bt.state = State.Canceled;
        
        emit BetCanceled(_id);
    }

    /// @notice Permet au *participant* de parier sur une proposition le montant transmis. Celui-ci doit être 
    ///         d'au moins la mise minimale la première fois. Possible uniquement lorsque le pari est dans l'état ouvert
    ///         et que la date limite de pari n'est pas passée.
    /// @dev Emmet un l'évenement 'NewBet' avec la somme nouvellement pariée qui peut être inférieur au montant transmis
    ///         si le plafond de mise maximale du participant est atteint.
    /// @param  _id identifiant du pari
    /// @param _outcomeId Index de la proposition sur laquelle parier
    function bet(uint _id, uint _outcomeId) public payable onlyParticipant(_id) {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.Open, "ImproperState");        // Etat 'ouvert' requis
        require(block.timestamp <= bt.betDeadlineTime, "DeadlineExpired");   // Date limite dépassée
        require(_outcomeId < bt.nbPossibilities, "OutOfBoundIndex");// index hors bornes
        
        Participant storage p = bt.participants[msg.sender];
        uint256 totalBet = p.totalBet; // copie mémoire pour optimiser

        // 1ère mise ?
        if (totalBet == 0) {
            // première fois : on vérifie qu'on a mis au moins le minimum 
            require(msg.value >= bt.minimumStake_micro * MICRO_ETH, "InsufficientStake"); // QUITTE sur err : mise insuffisante
            // mémorisation de la proposition
            p.betId = uint8(_outcomeId);
        } else {
            // on ne peut surenchérire que sur la même proposition
            require(_outcomeId == p.betId, "MultiChoiceNotAllowed"); // QUITTE sur err : choix multiples non permis
        }
        
        uint256 betAmount;
        
        // plafond non atteint ?
        uint256 totalMaxStake = bt.totalMaxStake_micro * MICRO_ETH;
        if (totalBet < totalMaxStake) {
            // limitation de la somme misée par le plafond
            betAmount = (totalBet + msg.value > totalMaxStake) ? totalMaxStake - totalBet : msg.value;
            // prise en compte du pari
            bt.betAmountByProp[_outcomeId] += betAmount;
            emit NewBet(_id, msg.sender, betAmount, _outcomeId);
            // mise à jour de la mise du joueur
            p.totalBet = totalBet + betAmount;
        }
        
        // remboursement du trop perçu le cas échéant
        if (msg.value > betAmount)
            msg.sender.transfer(msg.value - betAmount);
    }
    
    /// @notice Permet à une personne invitée par l'organisateur de participer, dans la limite de ce qui est permi,
    ///         et de miser en même temps sur une proposition le montant transmis. Celui-ci doit être d'au moins la
    ///         mise minimale. Possible uniquement lorsque le pari est dans l'état ouvert et que la date limite de
    ///         pari n'est pas passée.
    /// @dev Emmet un évenement 'NewParticiant' si le participant est ajouté. Emmet un l'évenement 'NewBet' avec la
    ///         somme nouvellement pariée qui peut être inférieur au montant transmis si le plafond de mise maximale
    ///         du participant est atteint.
    ///         L'organisateur du pari transmet à ses invités la clé privée correspondant à l'adresse publique du pari.
    ///         L'invité va signer un message comportant son adresse pour rendre la signature unique à chaque
    ///         paricipant et donc non réutilisable par un petit malin voulant s'inscrire au même pari et qui utiliserait
    ///         Etherscan pour récupéer une ancienne signature.
    /// @param  _id identifiant du pari
    /// @param  _rSign partie 'r' de la signature ECDSA du site web certifiant que le participant a bien été invité
    ///             par l'organisateur du pari.
    /// @param  _sSign partie 's' de la signature ECDSA
    /// @param  _vSign partie 'v' de la signature ECDSA
    /// @param _outcomeId Index de la proposition sur laquelle parier
    function participateAndBet(uint _id, bytes32 _rSign, bytes32 _sSign, uint8 _vSign, uint _outcomeId)
    public payable {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.Open, "ImproperState");    // Etat 'ouvert' requis
        require(block.timestamp <= bt.betDeadlineTime, "DeadlineExpired");   // Date limite dépassée
        require(_outcomeId < bt.nbPossibilities, "OutOfBoundIndex");// index hors bornes
        require(msg.value >= bt.minimumStake_micro * MICRO_ETH, "InsufficientStake"); // mise insuffisante
        
        // vérif signature de web3.eth.accounts.sign({participant})
        bytes32 msgHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n20", msg.sender));
        address signer = ecrecover(msgHash, _vSign, _rSign, _sSign);
        require(signer != address(0), "InvalidSignature"); // signature invalide (bug)
        require(signer == bt.guestPubKeyAdr, "WrongSigner");  // signataire incorrecte (bug, invitation modifiée/désactivée ou tentative de fraude)
        
        uint256 betAmount;
        
        // peut-on encore en ajouter un ?
        if (bt.nbParticipant < NB_MAX_PARTICIPANT) {
            Participant storage p = bt.participants[msg.sender];
            
            // on ajoute uniquement le participant s'il n'est pas déjà connu
            if (!p.knownUser) {
                // prise en compte du participant
                bt.nbParticipant++;
                emit NewParticiant(_id, msg.sender);

                // limitation de la somme misée par le plafond
                uint256 totalMaxStake = bt.totalMaxStake_micro * MICRO_ETH;
                betAmount = (msg.value > totalMaxStake) ? totalMaxStake : msg.value;
                // prise en compte du pari
                bt.betAmountByProp[_outcomeId] += betAmount;
                emit NewBet(_id, msg.sender, betAmount, _outcomeId);
                // maj info du Participant
                p.knownUser = true;
                p.betId = uint8(_outcomeId);
                p.totalBet = betAmount;
            }
        }
        
        // remboursement du trop perçu le cas échéant
        if (msg.value > betAmount)
            msg.sender.transfer(msg.value - betAmount);
    }
    
    /// @notice Création d'un pari, appelé par son futur organisateur. Possible si la création de paris n'est pas supendue.
    /// @dev    Emmet un évenement 'NewBetCreated' pour retourner l'identifiant du pari. Met le pari dans l'état créé.
    /// @param _externDescriptionId Identifiant 32bits de description externe du pari (lien avec la BDD du site web)
    /// @param _nbPossibilities Nombre de résultats possibles du pari. Au moins 2, 255 max.
    /// @param _betDeadlineTime Date limite pour parier (date UNIX / EPOC). Doit être > à la date actuelle
    /// @param _minimumStake_micro Mise minimale d'un participant en µEther
    /// @param _totalMaxStake_micro Mise maximale totale pour un participant en µEther. Doit être >= à la mise minimale
    /// @param _guestPubKeyAdr pseudo clé publique du pari pour authentifier un invité. 0x0 désactive `participate()`
    /// @param _outcomeId Index de la proposition sur laquelle parier
    function createBetAndBet(uint32 _externDescriptionId, uint _nbPossibilities, uint _betDeadlineTime,
                uint32 _minimumStake_micro, uint32 _totalMaxStake_micro, address _guestPubKeyAdr, uint _outcomeId)
    public payable {
        require(!m_createBetDisabled, "CreateBetDisabled");  // la création de pris est actuellement suspendue
        require(_nbPossibilities >= 2, "TooFewPossibility");   // Il faut au moins 2 possibilités
        require(_nbPossibilities < 2**8, "TooManyPossibilities");// 255 possibilités max
        require(_betDeadlineTime > block.timestamp, "DeadlineAlreadyPassed");   // Date limite déjà passée
        require(_totalMaxStake_micro >= _minimumStake_micro, "InvalidTotalMaxStake");// Mise maximale doit être au moins celle minimale
        require(_outcomeId < _nbPossibilities, "OutOfBoundIndex");// index hors bornes
        require(msg.value >= _minimumStake_micro * MICRO_ETH, "InsufficientStake"); // mise insuffisante
        
        uint nextId = m_bets.length;
        
        Bet storage bt = m_bets.push();
        bt.owner               = msg.sender;
        bt.betDeadlineTime     = uint32(_betDeadlineTime);
        bt.externDescriptionId = _externDescriptionId;
        //bt.state             = State.Open;
        bt.nbPossibilities     = uint8(_nbPossibilities);
        bt.betAmountByProp     = new uint256[](_nbPossibilities);
        bt.guestPubKeyAdr      = _guestPubKeyAdr;
        bt.minimumStake_micro  = _minimumStake_micro;
        bt.totalMaxStake_micro = _totalMaxStake_micro;
        
        emit NewBetCreated(nextId, msg.sender);
        
        Participant storage p = bt.participants[msg.sender];

        // prise en compte du participant
        bt.nbParticipant++;
        emit NewParticiant(nextId, msg.sender);
        
        // limitation de la somme misée par le plafond
        uint256 totalMaxStake = _totalMaxStake_micro * MICRO_ETH;
        uint256 betAmount = (msg.value > totalMaxStake) ? totalMaxStake : msg.value;
        // prise en compte du pari
        bt.betAmountByProp[_outcomeId] = betAmount;
        emit NewBet(nextId, msg.sender, betAmount, _outcomeId);
        // maj info du Participant
        p.knownUser = true;
        p.betId = uint8(_outcomeId);
        p.totalBet = betAmount;
        
        // remboursement du trop perçu le cas échéant
        if (msg.value > betAmount)
            msg.sender.transfer(msg.value - betAmount);
    }

    /// @notice Permet à *l'organisateur* de donner le résulat de pari. Possible uniquement lorsque le pari est dans
    ///         l'état ouvert et que la date limite de pari est passée.
    /// @dev le pari passe dans l'un des 2 états 'fermé'. Emmet l'événement 'BetClosed'.
    /// @param  _id identifiant du pari
    /// @param _outcomeId index de proposition gagnante dans la liste des possiblités
    function setBetOutcome(uint _id, uint _outcomeId) public onlyOrganizer(_id) {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.Open, "ImproperState");        // Etat 'ouvert' requis
        require(block.timestamp > bt.betDeadlineTime, "DeadlineNotPassed");  // Date limite pas encore échue
        require(_outcomeId < bt.nbPossibilities, "OutOfBoundIndex");//index hors bornes

        // avons nous un gagnant ?
        bool hasWinner = (bt.betAmountByProp[_outcomeId] > 0);
        if (hasWinner) {
            // calcul du pactole des gagants
            uint256 s;
            uint256[] storage tbl = bt.betAmountByProp; // accès tableau en registre pour optimiser
            uint length = tbl.length;                   // taille en cache pour optimiser
            for(uint i; i < length; i++)
                s += tbl[i];

            // mémo des gains pour calculer les parts de chacun des gagants
            bt.winningsAmount = s;

            bt.state = State.Closed_Winner;
        } else {
            bt.state = State.Closed_NoWinner;
        }
        
        // mémo résultat
        bt.outcomeId = uint8(_outcomeId);
        // retourne si on a un gagnant
        emit BetClosed(_id, hasWinner);
    }

    /// @notice Permet au *participant* de récupérer les mises pariées en cas de pari sans gagnant
    ///         ou annulé. Possible uniquement lorsque le pari est dans l'état 'annulé' ou 'Fermé sans gagnant'.
    /// @dev Le champs 'Participant.totalBet' est RAZ pour noter le remboursement
    /// @param  _id identifiant du pari
    /// @return amount_ le montant transféré. Permet de savoir d'avance ce qu'il y a à récupérer.
    function reimbursement(uint _id) public onlyParticipant(_id) returns(uint256 amount_) {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.Canceled || bt.state == State.Closed_NoWinner, "ImproperState"); // Etat incompatible
        
        Participant storage p = bt.participants[msg.sender];
        // qq chose à rembourser ?
        if (p.totalBet > 0)
        {
            amount_ = p.totalBet;
            p.totalBet = 0; // on RAZ l'état avant le transfert pour éviter un hack par réentrance

            emit Transfer(_id, msg.sender, amount_);
            msg.sender.transfer(amount_);
        }
    }

    /// @notice Permet au *participants gagnants* de récupérer leurs gains. Possible uniquement dans l'état
    ///         'Fermé avec gagnant'.
    /// @dev Le champs Participant.betAmountByProp[m_bets[_id].infos.outcomeId] est RAZ pour noter le retrait
    /// @param  _id identifiant du pari
    /// @return amount_ le montant transféré. Permet de savoir d'avance ce qu'il y a à récupérer.
    function withdrawWinnings(uint _id) public onlyParticipant(_id) returns(uint256 amount_) {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.Closed_Winner, "ImproperState"); // Etat 'fermé avec gagnant' requis
        
        Participant storage p = bt.participants[msg.sender];
        
        // a-t-il gagné ?
        if (p.betId == bt.outcomeId) {
            // calcul du gain commun au prorata de la mise sur la proposition gagante
            amount_ = (bt.winningsAmount * p.totalBet) / bt.betAmountByProp[bt.outcomeId];
        
            // qq chose à retirer ?
            if (amount_ > 0) {
                p.totalBet = 0; // on RAZ l'état avant le transfert pour éviter un hack par réentrance
    
                emit Transfer(_id, msg.sender, amount_);
                msg.sender.transfer(amount_);
            }
        }
    }