// Contract: BigPunisher
// Target function: function withdrawToOwner() public onlyOwner
 Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _setOwner(_msgSender());
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _setOwner(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _setOwner(newOwner);
    }

    function _setOwner(address newOwner) private {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

/**
 * @title Counters
 * @author Matt Condon (@shrugs)
 * @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number
 * of elements in a mapping, issuing ERC721 ids, or counting request ids.
 *
 * Include with `using Counters for Counters.Counter;`
 */
library Counters {
    struct Counter {
        // This variable should never be directly accessed by users of the library: interactions must be restricted to
        // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add
        // this feature: see https://github.com/ethereum/solidity/issues/4637
        uint256 _value; // default: 0
    }

    function current(Counter storage counter) internal view returns (uint256) {
        return counter._value;
    }

    function increment(Counter storage counter) internal {
    unchecked {
        counter._value += 1;
    }
    }

    function decrement(Counter storage counter) internal {
        uint256 value = counter._value;
        require(value > 0, "Counter: decrement overflow");
    unchecked {
        counter._value = value - 1;
    }
    }

    function reset(Counter storage counter) internal {
        counter._value = 0;
    }
}


contract BigPunisher is ERC721URIStorage, Ownable, AccessControl, ReentrancyGuard {

    using Counters for Counters.Counter;
    using Strings for uint256;
    using SafeMath for uint256;

    enum Edition{SILVER, GOLD, BLACK, DIAMOND}

    // EVENTS
    event WithdrawnToOwner(address indexed _operator, uint256 _ethWei);
    event WithdrawnToEntities(address indexed _operator, uint256 _ethWei);
    event NftMinted(address whoDone, address airdropAddress, Edition edition, uint256 number);
    event SaleChanged(bool saleOn, bool vipOnly);

    bytes32 public constant VIP_ROLE = keccak256("VIP_ROLE");
    bool public saleOn = false;
    bool public vipSaleOnly = false;
    struct AirDrop {
        address airdropAddress;
        Edition edition;
        uint256 count;
    }
    mapping(address => AirDrop[]) public airDrops;
    AirDrop[] public airDropsArray;

    Counters.Counter private silverCounter;
    Counters.Counter private goldCounter;
    Counters.Counter private blackCounter;
    Counters.Counter private diamondCounter;
    uint256 public maxMintingPerTime = 20;
    string private  _baseTokenURI;

    uint public silverNftCost = 14 * 10 ** 16;
    uint public goldNftCost = 25 * 10 ** 16;
    uint256 public silverSellingMax = 475;
    uint256 public goldSellingMax = 40;
    uint256 public silverSellingCount;
    uint256 public goldSellingCount;
    uint256 public silverAirDropCount;
    uint256 public goldAirDropCount;

    uint256 public silverTotalSupply = 500;
    uint256 public goldTotalSupply = 50;
    uint256 public blackTotalSupply = 5;
    uint256 public diamondTotalSupply = 1;

    uint256 private SILVER_FIRST_ID = 0;
    uint256 private GOLD_FIRST_ID = 500;
    uint256 private BLACK_FIRST_ID = 550;
    uint256 private DIAMOND_FIRST_ID = 555;

    //    Distribution  amount * rate / 1000
    uint256 private CELEBRITY_RATE = 400;
    uint256 private OWNER_RATE = 250;
    uint256 private PORTRAIT_RATE = 125;
    uint256 private THREE_D_RATE = 100;
    uint256 private MARKETING_AND_TECH_RATE = 100;
    uint256 private FRAME_RATE = 25;

    address payable public celebrityAddress = payable(0x0CD258756455D3Efc83B04cC7277B92769884af5);
    address payable public ownerAddress = payable(0xd6d4d7FAf57f22830d978f793d033d115E605962);
    address payable public portraitAddress = payable(0xCEfc1Aacf8579B572FB4ff84E4eD2a8963a6faf8);
    address payable public threeDimensionAddress = payable(0xBf3f449bAB21E2001bF94b105364b525022d88cE);
    address payable public marketingAndTechAddress = payable(0x07D409e34786467F335fF8b7A69e300Effe7E2cf);
    address payable public frameAddress = payable(0xB586D612DC53C9C632e3B039b4D8EdEc028daE70);


    constructor()
    ReentrancyGuard()
    ERC721("Big Punisher", "BIGPUN"){

        setBaseURI("https://ipfs.io/ipfs/QmWmfpcKams3VxJ692wEwE3fp7CKc5nEZmyoZ7Zykt6r3m/");
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);

    }


    // Modifiers
    modifier saleIsOn() {
        require(saleOn, "cannot purchase as the sale is off");
        _;
    }

    modifier isLimitedPerTx(uint256 _number) {
        require(_number <= maxMintingPerTime, "exceed the max minting limit per time");
        _;
    }

    modifier canMint(Edition _edition, uint256 _number){
        if (_edition == Edition.DIAMOND) {
            require(owner() == _msgSender(), "Ownable: caller is not the owner");
            require(diamondCounter.current() + _number <= diamondTotalSupply, "exceed the max supply limit for Diamond Edition");
            _;
        } else if (_edition == Edition.BLACK) {
            require(owner() == _msgSender(), "Ownable: caller is not the owner");
            require(blackCounter.current() + _number <= blackTotalSupply, "exceed the max supply limit for Black Edition");
            _;
        } else if (_edition == Edition.GOLD) {
            require(goldCounter.current() + _number <= goldTotalSupply, "exceed the max supply limit for Gold Edition");
            _;
        } else if (_edition == Edition.SILVER) {
            require(silverCounter.current() + _number <= silverTotalSupply, "exceed the max supply limit for Silver Edition");
            _;
        }
    }


    ///////////////////////////////////////////////   PUBLIC  ///////////////////////////////////////////////
    function createCollectible(Edition _edition, uint256 _number) external payable
    nonReentrant
    saleIsOn
    isLimitedPerTx(_number)
    canMint(_edition, _number)
    {
        require(_edition == Edition.SILVER || _edition == Edition.GOLD, "Can only mint Silver or Gold Nfts");
        if (vipSaleOnly) {
            require(hasRole(VIP_ROLE, msg.sender), "Caller is not VIP Whitelisted");
        }
        if (_edition == Edition.SILVER) {
            require(msg.value == (silverNftCost * _number), "Ether value sent is not correct");
            require(silverSellingCount.add(_number) <= silverSellingMax, "selling limit reached ");
            mint(_msgSender(), _number, silverCounter, SILVER_FIRST_ID, _edition);
            silverSellingCount = silverSellingCount + _number;
        }
        if (_edition == Edition.GOLD) {
            require(msg.value == (goldNftCost * _number), "Ether value sent is not correct");
            require(goldSellingCount.add(_number) <= goldSellingMax, "selling limit reached ");
            mint(_msgSender(), _number, goldCounter, GOLD_FIRST_ID, _edition);
            goldSellingCount += _number;
        }
    }

    function claimAirDrops() external nonReentrant {
        require(airDrops[_msgSender()][0].airdropAddress != address(0), "no airdrops found for the given address");
        AirDrop[] memory ads = airDrops[_msgSender()];
        for(uint i=0; i<ads.length; i++) {
            AirDrop memory ad = ads[i];
            if (ad.edition == Edition.SILVER) {
                require(silverAirDropCount + ad.count <= silverAirDropMax(), "exceed airdrop limit");
                mint(_msgSender(), ad.count, silverCounter, SILVER_FIRST_ID, ad.edition);
                silverAirDropCount += ad.count;
            }
            if (ad.edition == Edition.GOLD) {
                require(goldAirDropCount + ad.count <= goldAirDropMax(), "exceed airdrop limit");
                mint(_msgSender(), ad.count, goldCounter, GOLD_FIRST_ID, ad.edition);
                goldAirDropCount += ad.count;
            }
        }
        delete airDrops[_msgSender()];
    }


    function multiSend(uint256 amountToWithdraw) internal {

        uint celebrityBalance = amountToWithdraw.mul(CELEBRITY_RATE).div(1000);
        uint ownerBalance = amountToWithdraw.mul(OWNER_RATE).div(1000);
        uint portraitBalance = amountToWithdraw.mul(PORTRAIT_RATE).div(1000);
        uint threeDimensionBalance = amountToWithdraw.mul(THREE_D_RATE).div(1000);
        uint marketingAndTechBalance = amountToWithdraw.mul(MARKETING_AND_TECH_RATE).div(1000);
        uint frameBalance = amountToWithdraw.mul(FRAME_RATE).div(1000);

        require(
            amountToWithdraw == celebrityBalance + ownerBalance + portraitBalance + threeDimensionBalance + frameBalance + marketingAndTechBalance,
            "Something wrong happened in distribution calculation"
        );

        transferToAddressETH(celebrityAddress, celebrityBalance);
        transferToAddressETH(ownerAddress, ownerBalance);
        transferToAddressETH(portraitAddress, portraitBalance);
        transferToAddressETH(threeDimensionAddress, threeDimensionBalance);
        transferToAddressETH(marketingAndTechAddress, marketingAndTechBalance);
        transferToAddressETH(frameAddress, frameBalance);
    }

    function addToAirDrops(AirDrop[] memory _airDrops) public onlyOwner {
        for(uint i=0; i<_airDrops.length; i++){
            airDropsArray.push(_airDrops[i]);
            airDrops[_airDrops[i].airdropAddress].push(_airDrops[i]);
        }
    }

    function removeAirDrops(address[] memory airDropAddresses) public onlyOwner {
        for(uint i = 0; i < airDropAddresses.length; i++){
            delete airDrops[airDropAddresses[i]];
        }
    }

    function transferToAddressETH(address payable recipient, uint256 amount) private {
        recipient.transfer(amount);
    }

    function addressChange(
        address payable _celebrityAddress,
        address payable _tccAddress,
        address payable _portraitAddress,
        address payable _threeDimensionAddress,
        address payable _frameAddress,
        address payable _marketingAndTechAddress
    ) external onlyOwner{
        require(_celebrityAddress != address(0),"cant set dead address");
        require(_tccAddress != address(0),"cant set dead address");
        require(_portraitAddress != address(0),"cant set dead address");
        require(_threeDimensionAddress != address(0),"cant set dead address");
        require(_frameAddress != address(0),"cant set dead address");
        require(_marketingAndTechAddress != address(0),"cant set dead address");

        celebrityAddress = _celebrityAddress;
        ownerAddress = _tccAddress;
        portraitAddress = _portraitAddress;
        threeDimensionAddress = _threeDimensionAddress;
        frameAddress = _frameAddress;
        marketingAndTechAddress = _marketingAndTechAddress;
    }

    function totalSupply() public view returns (uint256) {
        return silverTotalSupply + goldTotalSupply + blackTotalSupply + diamondTotalSupply;
    }

    function totalMintedCount() public view returns (uint256) {
        return silverMintedCount() + goldMintedCount() + blackMintedCount() + diamondMintedCount();
    }

    function silverMintedCount() public view returns (uint256) {
        return silverCounter.current();
    }

    function goldMintedCount() public view returns (uint256) {
        return goldCounter.current();
    }

    function blackMintedCount() public view returns (uint256) {
        return blackCounter.current();
    }

    function diamondMintedCount() public view returns (uint256) {
        return diamondCounter.current();
    }

    function silverAirDropMax() public view returns(uint256) {
        return silverTotalSupply - silverSellingMax;
    }

    function goldAirDropMax() public view returns(uint256) {
        return goldTotalSupply - goldSellingMax;
    }

    function getAllAirDropsPerAddress(address airDropAddress) public view returns(AirDrop[] memory) {
        return airDrops[airDropAddress];
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////




    function ActivateSaleForAll() public onlyOwner {
        saleOn = true;
        vipSaleOnly = false;
        emit SaleChanged(saleOn, vipSaleOnly);
    }

    function DeactivateSaleForAll() public onlyOwner {
        saleOn = false;
        vipSaleOnly = false;
        emit SaleChanged(saleOn, vipSaleOnly);
    }

    function ActivateVipSaleOnly() public onlyOwner {
        saleOn = true;
        vipSaleOnly = true;
        emit SaleChanged(saleOn, vipSaleOnly);
    }

    /* Allow the owner set how max minting per time */
    function setMaxMintingPerTime(uint256 _maxMintingPerTime) public onlyOwner {
        maxMintingPerTime = _maxMintingPerTime;
    }

    /* Allow the owner set the base token uri */
    function setBaseURI(string memory _baseTokenURI_) public onlyOwner {
        _baseTokenURI = _baseTokenURI_;
    }

    /* The owner can set tokenURI if it's error */
    function setTokenURI(uint256 _tokenId, string memory _tokenURI) public onlyOwner {
        super._setTokenURI(_tokenId, _tokenURI);
    }

    /* rewrite tokenURI function */
    // function tokenURI(uint256 _tokenId) public view virtual override returns (string memory){
    //     require(_exists(_tokenId), "ERC721URIStorage: URI query for nonexistent token");

    //     if (bytes(_tokenURIs[_tokenId]).length > 0) {
    //         return string(abi.encodePacked("ipfs://", _tokenURIs[_tokenId]));
    //     }
    //     else if (bytes(_baseTokenURI).length > 0)
    //     {
    //         return string(abi.encodePacked(_baseTokenURI, _tokenId.toString()));
    //     }
    //     else
    //     {
    //         return super.tokenURI(_tokenId);
    //     }
    // }

    // when _howManyMEth = 1, it's 0.01 ETH
    function setSilverAndGoldNftCost(uint256 _silverNftCostInWei, uint256 _goldNftCostInWei) external onlyOwner {
        silverNftCost = _silverNftCostInWei;
        goldNftCost = _goldNftCostInWei;
    }


    /*
      minting number cannot exceed 255
    */
    function mint(address to, uint256 _number, Counters.Counter storage counter, uint256 _tokenFirstId, Edition edition) internal returns (bool)
    {
        require(_number > 0, "You need to indicate a number of nft to mint greater than 0");
        for (uint256 i= 0; i < _number; i++)
        {
            uint256 tokenID = counter.current() + _tokenFirstId;
            _safeMint(to, tokenID);
            counter.increment();
        }
        emit NftMinted(_msgSender(), to, edition, _number);

        return true;
    }

    function _baseURI() internal view virtual override returns (string memory){
        return _baseTokenURI;
    }



    // Only Owner