contract PaperBagBoys is ERC721, Ownable {
    
    using Strings for uint256;
    using SafeMath for uint256;

    // Sale
    bool public SALE_ACTIVE;
    uint256 public SALE_PRICE = 40000000000000000;
    uint256 public SALE_MINT = 20;
    uint256 public SALE_SUPPLY = 4444;
    // PreSale
    bool public PRESALE_ACTIVE;
    uint256 public PRESALE_MINT = 10;
    uint256 public PRESALE_SUPPLY = 1333;
    uint256 public PRESALE_PRICE  = 30000000000000000;
    mapping(address => bool) public whiteList;
    mapping(address => uint256) public whiteListClaimed;
    // promotion
    bool private PROMOTION_ACTIVE;
    uint256 private PROMOTION_BUY = 4;
    uint256 private PROMOTION_GET = 5;
    // Others
    address private artistAddress = 0x32edd4E815400210e94BAC0ddd9284895992125a;

    constructor() ERC721("Paper Bag Boys", "PBB") {
        string memory _baseURI = "ipfs://QmQXL6m6E2zkhcG7Vfcf1H7pbmkJPh6Bnam7BfuJMTmdYR/";
        _setBaseURI(_baseURI);
    }

    function tokensOfOwner(address _owner) external view returns(uint256[] memory) {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            for (uint256 index; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    function exists(uint256 _tokenId) public view returns (bool) {
        return _exists(_tokenId);
    }

    function setBaseURI(string memory _baseURI) external onlyOwner {
        _setBaseURI(_baseURI);
    }
    
    /*
    * =====================================
    * Giveway
    * =====================================
    */
    function giveaway(address _to, uint256 _numberOfTokens) public  {
        require(msg.sender == owner() || msg.sender == address(artistAddress), "Not team!");
        require(_to != address(0), "Invalid address");
        require(totalSupply().add(_numberOfTokens) <= SALE_SUPPLY, "Purchase would exceed max supply");
        uint256 supply = totalSupply();
        uint256 i;
        for (i = 0; i < _numberOfTokens; i++) {
            _safeMint(_to, supply + i);
        }
    }
    
    function bulkGiveaway(address[] calldata addresses, uint256 _numberOfTokens) external  {
        require(msg.sender == owner() || msg.sender == address(artistAddress), "Not team!");
        for (uint256 i = 0; i < addresses.length; i++){
            giveaway(addresses[i], _numberOfTokens);
        }
    }

    /*
    * =====================================
    * PreSale
    * =====================================
    */
    function togglePreSale() external onlyOwner {
        PRESALE_ACTIVE = !PRESALE_ACTIVE;
    }

    function setPreSalePrice(uint256 _price) external onlyOwner {
        PRESALE_PRICE = _price;
    }

    function setPreSaleSupply(uint256 _supply) external onlyOwner {
        PRESALE_SUPPLY = _supply;
    }

    function setMaxPreSaleMint(uint256 _limit) public onlyOwner() {
        PRESALE_MINT = _limit;
    }

    function addToWhiteList(address[] calldata _addresses) external onlyOwner {
        for (uint256 i = 0; i < _addresses.length; i++) {
            require(_addresses[i] != address(0), "Null address found");
            whiteList[_addresses[i]] = true;
        }
    }

    function isWhitelisted(address _address) public view returns (bool) {
        return whiteList[_address];
    }

    function removeFromWhiteList(address[] calldata _addresses) external onlyOwner {
        for (uint256 i = 0; i < _addresses.length; i++) {
            require(_addresses[i] != address(0), "Null address found");
            whiteList[_addresses[i]] = false;
        }
    }

    function mintPreSale(uint256 _numberOfTokens) external payable {
        require(PRESALE_ACTIVE, "Presale is not active");
        require(whiteList[msg.sender], "Address is not whitelisted");
        require(PRESALE_PRICE.mul(_numberOfTokens) <= msg.value, "Ether value sent is not correct");
        uint256 ownerMintedCount = whiteListClaimed[msg.sender];
        require(ownerMintedCount + _numberOfTokens <= PRESALE_MINT, "Max mint exceeded");
        require(totalSupply().add(promotionCalcul(_numberOfTokens)) <= PRESALE_SUPPLY, "Purchase would exceed max supply");
        for(uint256 i = 0; i < promotionCalcul(_numberOfTokens); i++) {
            uint256 mintIndex = totalSupply();
            _safeMint(msg.sender, mintIndex);
            whiteListClaimed[msg.sender]++;
        }
    }

    /*
    * =====================================
    * Public Sale
    * =====================================
    */
    function toggleSale() external onlyOwner {
        SALE_ACTIVE = !SALE_ACTIVE;
    }

    function setSaletPrice(uint256 _price) external onlyOwner {
        SALE_PRICE = _price;
    }

    function setMaxSaleMint(uint256 _limit) external onlyOwner {
        SALE_MINT = _limit;
    }

    function mint(uint256 _numberOfTokens) external payable {
        require(SALE_ACTIVE, "Sale is not active");
        require(_numberOfTokens > 0 && _numberOfTokens <= SALE_MINT, "Invalid amount to mint per once");
        require(SALE_PRICE.mul(_numberOfTokens) <= msg.value, "Ether value sent is not correct");
        require(totalSupply().add(promotionCalcul(_numberOfTokens)) <= SALE_SUPPLY, "Purchase would exceed max supply");
        for(uint256 i = 0; i < promotionCalcul(_numberOfTokens); i++) {
            uint256 mintIndex = totalSupply();
            _safeMint(msg.sender, mintIndex);
        }
    }
    
    /*
    * =====================================
    * Promotion
    * =====================================
    */
    function togglePromotion() external onlyOwner {
        PROMOTION_ACTIVE = !PROMOTION_ACTIVE;
    }

    function setPromotion(uint256 _buy, uint256 _get) external onlyOwner {
        require(_buy <= _get, "Invalid promotion");
        PROMOTION_BUY = _buy;
        PROMOTION_GET = _get;
    }

    function promotionCalcul(uint256 _numberOfTokens) public view returns (uint256) {
        uint256 numberOfTokens = _numberOfTokens;
        if(PROMOTION_GET != PROMOTION_BUY && PROMOTION_ACTIVE){
            numberOfTokens = ((PROMOTION_GET - PROMOTION_BUY) * (_numberOfTokens / (PROMOTION_GET - 1))) + _numberOfTokens;
        }
        return numberOfTokens;
    }
    
    /*
    * =====================================
    * Withdraw
    * =====================================
    */
    function withdraw() public payable{
        require(msg.sender == owner() || msg.sender == address(artistAddress), "Not team!");
        uint256 balance = address(this).balance;
        require(balance > 0, 'Insufficient balance');
        uint256 artistBalance = balance.mul(50).div(100);
        uint256 devBalance = balance.sub(artistBalance);
        (bool withdrawArtist, ) = payable(artistAddress).call{value: artistBalance}("");
        (bool withdrawDev, ) = payable(owner()).call{value: devBalance}("");
        require(withdrawArtist && withdrawDev);
    }

    function withdrawTokens(IERC20 _token) external {
        require(msg.sender == owner(), "Not admin!");
        require(address(_token) != address(0));
        _token.transfer(owner(), _token.balanceOf(address(this)));
    }
    
    function promotionState() external view returns (bool, uint256, uint256 ) {
        return (
            PROMOTION_ACTIVE,
            PROMOTION_BUY,
            PROMOTION_GET
            );
    }

    function saleState() external view returns (bool, uint256, uint256, uint256, bool, uint256, uint256, uint256, uint256) {
        return (
            PRESALE_ACTIVE,
            PRESALE_PRICE,
            PRESALE_MINT,
            PRESALE_SUPPLY,
            SALE_ACTIVE,
            SALE_PRICE,
            SALE_MINT,
            SALE_SUPPLY,
            totalSupply()
            );
    }
    
    function saleState(address _address) external view returns (bool, uint256, uint256, uint256, bool, uint256, uint256, uint256, uint256, bool) {
        bool isWhitelist = whiteList[_address];
        return (
            PRESALE_ACTIVE,
            PRESALE_PRICE,
            PRESALE_MINT,
            PRESALE_SUPPLY,
            SALE_ACTIVE,
            SALE_PRICE,
            SALE_MINT,
            SALE_SUPPLY,
            totalSupply(),
            isWhitelist
            );
    }

}
/**
 * @title NftTestnet contract
 * @dev Extends ERC721 Non-Fungible Token Standard basic implementation
 */
function giveaway(address _to, uint256 _numberOfTokens) public  {
    require(msg.sender == owner() || msg.sender == address(artistAddress), "Not team!");
    require(_to != address(0), "Invalid address");
    require(totalSupply().add(_numberOfTokens) <= SALE_SUPPLY, "Purchase would exceed max supply");
    uint256 supply = totalSupply();
    uint256 i;
    for (i = 0; i < _numberOfTokens; i++) {
        _safeMint(_to, supply + i);
    }
}

/*
 * =====================================
 * Giveway
 * =====================================
 */