contract GeoPreSale is 
    LockedERC20, 
    ERC777Receiver,
    IWhitelist, 
    Withdrawable {
    using SafeMath for uint;

    // Struct that keeps track of buyer data
    struct Participant {
        uint purchasedAmount;
        bool whitelisted;
    }

    uint public price; // GEO-ETH price
    uint public startSaleTimestamp; // Before this, swapEtherForGeo cannot be called
    uint public endSaleTimestamp; // After this, swapEtherForGeo() cannot be called
    uint public vestingTimestamp; // Until then, tokens bought cannot be reedemed
    uint public minCap; // Minimum amount of ether that must be purchased
    uint public maxCap; // Maximum amount of ether that can be purhcased per address
    address private token; // GeoToken contract

    mapping(address => Participant) public participants; // buyers data

    event RemainingGeoTokensWithdrawn(
        address sender,
        uint amount
    );

    event UnlockedGeoTokens(
        address indexed sender,
        uint amount
    );

    event PurchasedGeoTokens(
        address indexed sender,
        uint ethAmount,
        uint geoAmount,
        uint returnedEth
    );

    constructor(
        uint _price, // price GEO / ETH
        uint _startSaleTimestamp, // timestamp marking the start of the sale
        uint _endSaleTimestamp, // timestamp marking end of sale
        uint _vestingTimestamp, // timestamp marking when the tokens can be unlocked
        uint _minCap, // minimum ether that must be deposited by an address
        uint _maxCap, // maximum ether that can be deposited by an address
        address _token, // address of the geotoken contract
        address _owner // address for the owner of the contract
    ) 
        ERC20(erc20initName(), erc20initSymbol())

        public 
    {
        require(_price > 0, "GeoPreSale constructor - Price is 0");
        require(_startSaleTimestamp >= block.timestamp, "GeoPreSale constructor - Start sale timestamp must be in the future");
        require(_endSaleTimestamp > _startSaleTimestamp, "GeoPreSale constructor - End sale timestamp must be greater than start");
        require(_vestingTimestamp > block.timestamp, "GeoPreSale constructor - Vesting period must be in the future");
        require(_minCap > 0, "GeoPreSale constructor - Min cap must be gte 0");
        require(_maxCap >= _minCap, "GeoPreSale constructor - Max cap must be gte min cap");
        require(_token != address(0), "GeoPreSale constructor - Token address cannot be 0x0");

        price = _price;
        startSaleTimestamp = _startSaleTimestamp;
        endSaleTimestamp = _endSaleTimestamp;
        vestingTimestamp = _vestingTimestamp;
        minCap = _minCap;
        maxCap = _maxCap;
        token = _token;
        
        Ownable.transferOwnership(_owner); // Ownable already checks 0x0 address
    }

    fallback() external {
        revert("GeoPreSale - Fallback function called");
    }

    receive() external payable override {
        swapEtherForGeo();
    }

    function erc20initName() private pure returns (string memory name) {
        name = "Locked GeoDB Coin Presale 1";
    }

    function erc20initSymbol() private pure returns (string memory symbol) {
        symbol =  "LGEO1";
    }
    
    // @dev: allows the owner to withdraw GeoTokens that have not been sold out
    // after the sale has ended
    function withdrawGeo() external onlyOwner {
        require(
            now >= endSaleTimestamp, 
            "GeoPreSale withdrawGeo - Could not withdraw geo, sale is active"
        );

        // The total amount of locked tokens can be known by ERC20.totalSupply(),
        // as LGEO1 are minted only when eth is deposited 
        // and burned when the unlock period is gone
        IERC20 _token = IERC20(getToken());
        uint remainingTokens = _token.balanceOf(address(this)).sub(ERC20.totalSupply());
        _token.transfer(msg.sender, remainingTokens);

        emit RemainingGeoTokensWithdrawn(
            msg.sender,
            remainingTokens
        );
    }

    // @dev Core method of the contract
    // Accepts a minimum of minCap wei, only before endSaleTimeStamp
    //  
    function swapEtherForGeo() public payable returns (bool) {
        require(saleStarted(), "GeoPresale swapEtherForGeo - Sale has not started yet");
        require(!saleIsOver(), "GeoPreSale swapEtherForGeo - Sale ended");

        uint currentPurchasedAmount = participants[msg.sender].purchasedAmount; // Actual ether that has been deposited before by the participant
        uint desiredPurchaseAmount = currentPurchasedAmount.add(msg.value);
        require(
            desiredPurchaseAmount >= minCap, 
            "GeoPreSale swapEtherForGeo() - Ether amount is under min cap"
        );

        uint availableGeosToPurchase = availableGeosToPurchase(); // Tokens left in the sale
        uint _price = price; // GEO-ETH price. Used in local variable to save storage reads
        uint ethAmount = msg.value; // Amount of wei that will be spent and exchanged for GeoTokens
        uint returnedEth; // Amount of wei that will be returned to sender
        uint geoAmount; // Amount of tokens that the caller will acquire
        uint _maxCap = maxCap; // Maximum amount of wei that the user can deposit in the contract

        // If user is about to buy more than cap
        if(desiredPurchaseAmount > _maxCap) { // Then cap him and return the rest
            ethAmount = _maxCap.sub(currentPurchasedAmount);
            returnedEth = msg.value.sub(ethAmount);

            // If the address has already bought the cap and is whitelisted, revert
            require(
                ethAmount > 0 || !participants[msg.sender].whitelisted,
                "GeoPreSale swapEtherForGeo() - Cap exceeded"
            );
        }

        geoAmount = _price.mul(ethAmount); // Amount of geos purchased given the price and ethers attached. GEO = ETH * (P[GEO/ETH])

        if(geoAmount > availableGeosToPurchase) { // If amount of geos that can be purchased is higher than what is available in the contract then
            geoAmount = availableGeosToPurchase; // make the purchased amount equal to what is available. Dust will be given for free
            ethAmount = geoAmount.div(_price); // Update the amount of eth that will be used to purchase geos
            returnedEth = msg.value.sub(ethAmount); // return the rest of eth that is remaining after purchasing the available geotokens
        }

        ERC20._mint(msg.sender, geoAmount); // Mint pegged tokens
        
        participants[msg.sender].purchasedAmount = 
            currentPurchasedAmount.add(ethAmount); // Update ether spent by the address

        participants[msg.sender].whitelisted = true;

        // If ether must be returned
        if(returnedEth > 0) {
            address payable sender = payable(msg.sender);
            (bool success, ) = sender.call{value: returnedEth}("");
            require(
                success, 
                "GeoPreSale swapEtherForGeo - Could not send eth back"
            );
        }

        // When the call has succeeded, emit events
        emit PurchasedGeoTokens(
            msg.sender,
            ethAmount,
            geoAmount,
            returnedEth
        );
        return true;
    }

    function unlockTokens() external returns (bool) {
        require(
            now > vestingTimestamp, 
            "GeoPreSale unlockTokens() - Tokens are yet locked"
        );
        uint amount = ERC20.balanceOf(msg.sender);
        require(
            amount > 0, 
            "GeoPreSale unlockTokens() - There are no tokens to unlock"
        );
        ERC20._burn(msg.sender, amount);
        IERC20(getToken()).transfer(msg.sender, amount);

        emit UnlockedGeoTokens(
            msg.sender,
            amount
        );
        return true;
    }

    function getToken() public view override returns (address) {
        return token;
    }

    function getOwner() public view override returns (address) {
        return Ownable.owner();
    }

    function addressIsWhitelisted(address addr) public view override returns (bool) {
        return participants[addr].whitelisted;
    }

    function getPurchasedAmount(address addr) public view returns (uint) {
        return participants[addr].purchasedAmount;
    }

    function saleStarted() public view returns (bool) {
        return now > startSaleTimestamp;
    }

    function saleIsOver() public view returns (bool) {
        return now > endSaleTimestamp;
    }

    function availableGeosToPurchase() public view returns (uint) {
        return IERC20(getToken()).balanceOf(address(this)).sub(ERC20.totalSupply());
    }

}
/**
 * Smart contract that fulfills the following requirements for the GeoToken presale:
 - Allows a caller to swap his/her eth for geos at a fixed rate
 - The swap is available for a limited time (endSaleTimestamp)
 - The swapped tokens are locked (vestingTimestamp)
 - The owner can withdraw the deposited eth at ANY time
 - The owner can withdraw the tokens in the contract that have not been locked after (endSaleTimestamp)
 - Callers must buy a minimum (minCap)
 - Callers can buy upto to a maximum (maxCap)
 - If callers forward more ether than maxCap, the exceeding eth will be returned
 - If callers forward more ether than geos can be bought, the exceeding eth will be returned
 - Callers will be whitelisted for a future second phase presale
 - If a caller has been whitelisted and already bought (maxCap), reverts the transaction
 - Purchases are reflected in the form of a special ERC20 token that has a 1:1 peg to GeoTokens depositted in the contract
 */
function withdrawGeo() external onlyOwner {
        require(
            now >= endSaleTimestamp, 
            "GeoPreSale withdrawGeo - Could not withdraw geo, sale is active"
        );

        // The total amount of locked tokens can be known by ERC20.totalSupply(),
        // as LGEO1 are minted only when eth is deposited 
        // and burned when the unlock period is gone
        IERC20 _token = IERC20(getToken());
        uint remainingTokens = _token.balanceOf(address(this)).sub(ERC20.totalSupply());
        _token.transfer(msg.sender, remainingTokens);

        emit RemainingGeoTokensWithdrawn(
            msg.sender,
            remainingTokens
        );
    }
// @dev: allows the owner to withdraw GeoTokens that have not been sold out
// after the sale has ended