// Contract: PrivateBet
// Target function: function setBetOutcome(uint _id, uint _outcomeId) public onlyOrganizer(_id)
 la structure de donnés publiques 
    function getBetInfo(uint _id, bytes32 _rSign, bytes32 _sSign, uint8 _vSign) public view
    returns(BetInfo memory inf_)  {
        // only known user (créateur du contract, organisateur, invité ou participant)
        require(msg.sender == m_owner || msg.sender == m_bets[_id].owner ||
                msg.sender == m_bets[_id].guestPubKeyAdr || // `&& m_bets[_id].guestPubKeyAdr != address(0)` innutile grâce à "InvalidSignature"
                m_bets[_id].participants[msg.sender].knownUser, "OnlyKnownUser");

        // vérif signature du msg pour garantir l'authenticité de l'appelant
        address signer = ecrecover(AUTH_MSG, _vSign, _rSign, _sSign);
        require(signer != address(0), "InvalidSignature"); // signature invalide (bug)
        require(signer == msg.sender, "WrongSigner");  // signataire incorrecte (bug ou tentative d'usurpation d'identité)
        
        Bet storage bt = m_bets[_id];
        inf_.owner               = bt.owner;
        inf_.outcomeId           = bt.outcomeId;
        inf_.nbParticipant       = bt.nbParticipant;
        inf_.nbPossibilities     = bt.nbPossibilities;
        inf_.state               = bt.state;
        inf_.externDescriptionId = bt.externDescriptionId;
        inf_.betDeadlineTime     = bt.betDeadlineTime;
        inf_.minimumStake_micro  = bt.minimumStake_micro;
        inf_.totalMaxStake_micro = bt.totalMaxStake_micro;
        inf_.betAmountByProp     = bt.betAmountByProp;
    }

    /// @notice Permet à *l'organisateur* d'inscire un ou plusieurs participants, dans la limite de ce qui est
    ///         permi et uniquement lorsque le pari est dans l'état ouvert.
    /// @dev Emmet un évenement par participant ajouté. Le nombre de participant ajoutés peut être inférieur à
    ///         ce qui est attendu si la limite max est atteinte ou en cas de doublons avec des participants déjà inscrits.
    /// @param  _id identifiant du pari
    /// @param _adrParticipants Tableau d'adresses Ethereum des nouveaux participants
    function addParticipants(uint _id, address[] memory _adrParticipants) public onlyOrganizer(_id) {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.Open, "ImproperState");    // Etat 'ouvert' requis
        
        // parcours du tableau reçu dans la limite du nbr de participant max autorisés
        for (uint newUsers; newUsers < _adrParticipants.length && bt.nbParticipant < NB_MAX_PARTICIPANT; newUsers++) {
            address p = _adrParticipants[newUsers];
            // on ajoute uniquement le participant s'il est valide et n'est pas déjà connu
            if (p != address(0) && !bt.participants[p].knownUser) {
                bt.participants[p].knownUser = true;

                // prise en compte du participant
                bt.nbParticipant++;
                emit NewParticiant(_id, p);
            }
        }
    }

    /// @notice Permet à une personne invitée par l'organisateur de participer, dans la limite de ce qui est permi
    ///         et uniquement lorsque le pari est dans l'état ouvert.
    /// @dev Emmet un évenement 'NewParticiant' si le participant est ajouté.
    ///         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
    function participate(uint _id, bytes32 _rSign, bytes32 _sSign, uint8 _vSign) public {
        Bet storage bt = m_bets[_id];
        require(bt.state == State.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);
    }