contract Portal is  Pausable, Initializable {
    using SafeMath for uint256;
    using SafeMath for uint32;

    uint256 interestRate ;
    uint256 MAX_UINT;
    uint256 ONE_DAY ;
    address public xioExchangeAddress ;
    address public xioContractAddress ;
    address public uniswapFactoryAddress ;
    uint256 portalId;

    mapping (address=>StakerData[]) public stakerData;
    mapping (uint256=>PortalData) public portalData;

    //for testing
    uint256 ONE_MINUTE;
    mapping (address=>bool) internal whiteListed;


    struct StakerData {
        uint256 portalId;
        address publicKey;
        uint256 stakeQuantity;
        uint256 stakeDurationTimestamp;
        uint256 stakeInitiationTimestamp;
        string outputTokenSymbol;
    }

    struct PortalData {
        uint256 portalId;
        address tokenAddress;
        address tokenExchangeAddress;
        string outputTokenSymbol;
        uint256 xioStaked;
    }

    function initialize() public initializer{
        _paused = false;
        _owner = msg.sender;
        interestRate = 684931506849315;
        MAX_UINT = 2**256 - 1;
        ONE_DAY = 24*60*60;
        xioExchangeAddress = 0x7B6E5278a14d5318571d65aceD036d09c998C707;
        xioContractAddress = 0x0f7F961648aE6Db43C75663aC7E5414Eb79b5704;
        uniswapFactoryAddress = 0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95;
        portalId = 0;
        ONE_MINUTE = 60;
    }

    event DataEntered(address staker, uint256  portalId, uint256 quantity); // When data is entered into the mapping
    event Tranferred(address staker, uint256  portalId, uint256 quantity, string symbol); // When bought tokens are transferred to staker
    event Bought(address staker, uint256  portalId, uint256 _tokensBought, string symbol); // When tokens are bought
    event Transfer(address to, uint256 value); // When tokens are withdrawn


    /* @dev to get interest rate of the portal */
    function getInterestRate() public view returns(uint256){
        return interestRate;
    }

    /* @dev to get exchange rate of XIO to ETH
    *  @param _amount, xio amount
    */
    function getXIOtoETH(uint256 _amount) public view returns (uint256){
        return UniswapExchangeInterface(xioExchangeAddress).getTokenToEthInputPrice(_amount);
    }

    /* @dev to get exchange rate of ETH to ALT
    *  @param _amount, xio amount
    *  @param _outputTokenAddressExchange, exchange address of output token on uniswap
    */
    function getETHtoALT(uint256 _amount, address _outputTokenAddressExchange) public view returns (uint256){
        return UniswapExchangeInterface(_outputTokenAddressExchange).getEthToTokenInputPrice(_amount);
    }

    /* @dev to get array's lenght of staker data // for front end feasiblity
    *  @param _address, address of staker
    */
    function getArrayLengthOfStakerData(address _address) public view returns(uint256){
        return stakerData[_address].length;
    }


    /* @dev to check if given address is contract's or not
    *  @param _addr, public address
    */
    function isContract(address _addr) internal view returns (bool){
        uint32 size;
        assembly {
            size := extcodesize(_addr)
        }
        return (size > 0);
    }

    /* @dev to check portal if it already exists or not
    *  @param _tokenAddress, address of output token
    */
    function checkPortalExists(address _tokenAddress) internal view returns (bool){
        bool exists;
        for(uint256 i=0; ;i++){
            if(portalData[i].tokenAddress == address(0)){
                exists = false;
                break;
            } else if(portalData[i].tokenAddress == _tokenAddress){
                exists = true;
                break;
            }
        }
        return exists;
    }


    /* @dev stake function which calls uniswaps exchange to buy output tokens and send them to the user.
    *  @param _quantity , xio token quanity user has staked (in wei)
    *  @param _xioQuantity, xio interest generated upon the days (in wei)
    *  @param _tokensBought, how much tokens are bought from the uniswaps exchange (in wei)
    *  @param _portalId, portal id of the exchange.
    *  @param _symbol, bought token symbol
    *  @param _outputTokenAddress, bought token ERC20 address
    *  @param _days, how much days he has staked (in days)
    */
    function stakeXIO( address _outputTokenAddress, uint256 _days, uint256 _xioQuantity, uint256 _tokensBought, uint256 _portalId) public whenNotPaused returns (bool) {

        require(_days>0, "Invalid Days");  // To check days
        require(_xioQuantity > 0 , "Invalid XIO quantity"); // To verify XIO quantity
        require(_outputTokenAddress != address(0),"0 address not allowed"); // To verify output token address
        require(isContract(_outputTokenAddress) != false, "Not a contract address"); // To verify address is contract or not
        require(portalData[_portalId].tokenAddress != address(0), "Portal does not exists"); // To verify portal info
        require(whiteListed[msg.sender] == true, "Not whitelist address"); //To verify whitelisters
        require(portalData[_portalId].tokenAddress == _outputTokenAddress, "Wrong portal"); //To check correct portal

        // stakerData[msg.sender].push(StakerData(_portalId, msg.sender, _xioQuantity, _days*ONE_DAY, block.timestamp, _symbol));

        stakerData[msg.sender].push(StakerData(_portalId, msg.sender, _xioQuantity, _days.mul(ONE_MINUTE), block.timestamp, portalData[_portalId].outputTokenSymbol));

        emit DataEntered(msg.sender,_portalId,_xioQuantity);

        portalData[_portalId].xioStaked = portalData[_portalId].xioStaked.add(_xioQuantity)  ;


        Token(xioContractAddress).transferFrom(msg.sender, address(this),_xioQuantity);

        uint256 soldXIO = (_xioQuantity.mul(interestRate).mul(_days)).div(1000000000000000000);

        uint256 bought = UniswapExchangeInterface(xioExchangeAddress).tokenToTokenSwapInput(soldXIO,_tokensBought,1,1839591241,_outputTokenAddress);

        if(bought > 0){
            emit Bought(msg.sender,_portalId,_tokensBought,portalData[_portalId].outputTokenSymbol);
            Token(portalData[_portalId].tokenAddress).transfer(msg.sender,_tokensBought);
            emit Tranferred(msg.sender,_portalId,_tokensBought,portalData[_portalId].outputTokenSymbol);
            return true;
        }
        return false;
    }

    /* @dev withdrwal function by which user can withdraw their staked xio
    *  @param _amount , xio token quanity user has staked (in wei)
    */
    function withdrawXIO(uint256 _amount) public whenNotPaused {
        require(_amount>0, "Amount should be greater than 0");
        uint256 withdrawAmount = 0;
        StakerData[] storage stakerArray= stakerData[msg.sender];
        for(uint256 i=0; i<stakerArray.length;i++){
            if((stakerArray[i].stakeInitiationTimestamp.add(stakerArray[i].stakeDurationTimestamp)  <= block.timestamp) && (stakerArray[i].publicKey != address(0))){
                if(_amount > stakerArray[i].stakeQuantity){
                    stakerArray[i].publicKey = 0x0000000000000000000000000000000000000000;
                    _amount = _amount.sub(stakerArray[i].stakeQuantity);
                    withdrawAmount = withdrawAmount.add(stakerArray[i].stakeQuantity);
                    portalData[stakerArray[i].portalId].xioStaked = portalData[stakerArray[i].portalId].xioStaked.sub(stakerArray[i].stakeQuantity);
                    stakerArray[i].stakeQuantity = 0;
                }
                else if(_amount == stakerArray[i].stakeQuantity){
                    stakerArray[i].publicKey = 0x0000000000000000000000000000000000000000;
                    withdrawAmount = withdrawAmount.add(stakerArray[i].stakeQuantity);
                    stakerArray[i].stakeQuantity = 0;
                    portalData[stakerArray[i].portalId].xioStaked = portalData[stakerArray[i].portalId].xioStaked.sub(_amount);
                    break;
                }else if(_amount < stakerArray[i].stakeQuantity){
                    stakerArray[i].stakeQuantity = stakerArray[i].stakeQuantity.sub(_amount);
                    withdrawAmount = _amount;
                    portalData[stakerArray[i].portalId].xioStaked = portalData[stakerArray[i].portalId].xioStaked.sub( _amount);
                    break;
                }

            }
        }
        require(withdrawAmount !=0, "Not Transferred");
        Token(xioContractAddress).transfer(msg.sender,withdrawAmount);
        emit Transfer(msg.sender,withdrawAmount);
    }

    /* @dev incase of emergency owner can withdraw all the funds */
    function withdrawTokens() public onlyOwner whenNotPaused{
        uint256 balance = Token(xioContractAddress).balanceOf(address(this));
        Token(xioContractAddress).transfer(_owner,balance);
    }


    /* @dev to add portal into the contract
    *  @param _tokenAddress, address of output token
    */
    function addPortal(address _tokenAddress) public onlyOwner whenNotPaused returns(bool) {
        require(_tokenAddress != address(0), "Zero address not allowed");
        require(checkPortalExists(_tokenAddress) == false , "Portal already exists");
        address exchangeAddress = UniswapFactoryInterface(uniswapFactoryAddress).getExchange(_tokenAddress);
        require(exchangeAddress != address(0));
        string memory symbol = Token(_tokenAddress).symbol();
        portalData[portalId] = PortalData(portalId, _tokenAddress, exchangeAddress, symbol, 0);
        portalId = portalId.add(1);
        return true;
    }

    /* @dev to delete portal into the contract
    *  @param _portalId, portal Id of portal
    */
    function removePortal(uint256 _portalId) public onlyOwner whenNotPaused returns(bool) {
        require(portalData[_portalId].tokenAddress != address(0),"Portal does not exist");
        uint256 xioAmount = portalData[_portalId].xioStaked;
        portalData[_portalId] = PortalData(_portalId, 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, "NONE", xioAmount);
        return true;
    }

    /* @dev to set interest rate. Can only be called by owner
    *  @param _rate, interest rate (in wei)
    */
    function setInterestRate(uint256 _rate) public onlyOwner whenNotPaused returns(bool) {
        require(_rate != 0, "Rate connot be zero");
        interestRate = _rate;
    }

    /* @dev to allow XIO exchange max XIO tokens from the portal, can only be called by owner */
    function allowXIO() public onlyOwner whenNotPaused returns(bool) {
        return Token(xioContractAddress).approve(xioExchangeAddress, MAX_UINT);
    }

    /* @dev to add whitelist addresses // for front end feasiblity
    *  @param __staker, array of staker address
    */
    function addWhiteListAccount(address[] memory _staker) public onlyOwner whenNotPaused {
        for(uint8 i=0; i<_staker.length;i++){
            require(_staker[i] != address(0), "Zero address not allowed");
            whiteListed[_staker[i]]=true;
        }
    }

    /* @dev to update exchange address
    *  @param _exchangeAddress, xio exchange address
    */
    function setXIOExchangeAddress(address _exchangeAddress) public onlyOwner whenNotPaused {
        require(_exchangeAddress != address(0), "Zero address not allowed");
        xioExchangeAddress = _exchangeAddress;
    }

    /* @dev to update factory address
    *  @param _factoryAddress, factory address of uniswap
    */
    function setUniswapFactoryAddress(address _factoryAddress) public onlyOwner whenNotPaused {
        require(_factoryAddress != address(0), "Zero address not allowed");
        uniswapFactoryAddress = _factoryAddress;
    }

}
function isContract(address _addr) internal view returns (bool){
    uint32 size;
    assembly {
        size := extcodesize(_addr)
    }
    return (size > 0);
}

/* @dev to check if given address is contract's or not
 *  @param _addr, public address
 */