pragma solidity ^0.8.0;


abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    
    constructor() {
        _setOwner(_msgSender());
    }

    
    function owner() public view virtual returns (address) {
        return _owner;
    }

    
    modifier onlyOwner() {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
        _;
    }

    
    function renounceOwnership() public virtual onlyOwner {
        _setOwner(address(0));
    }

    
    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);
    }
}

interface EthereansInterface {
    function balanceOf(address owner) external view returns (uint256 balance);

    function ownerOf(uint256 tokenId) external view returns (address owner);
}

interface InvaderTokenInterface {
    function claimReward(address owner, uint amount) external;

    function burnFrom(address from, uint amount) external;
}

interface EmpireDropsInterface {
    function mint(address from, uint tokenId) external;

}

library SafeMath {
    
    function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            uint256 c = a + b;
            if (c < a) return (false, 0);
            return (true, c);
        }
    }

    
    function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b > a) return (false, 0);
            return (true, a - b);
        }
    }

    
    function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            
            
            
            if (a == 0) return (true, 0);
            uint256 c = a * b;
            if (c / a != b) return (false, 0);
            return (true, c);
        }
    }

    
    function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a / b);
        }
    }

    
    function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a % b);
        }
    }

    
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }

    
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return a - b;
    }

    
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        return a * b;
    }

    
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return a / b;
    }

    
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return a % b;
    }

    
    function sub(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b <= a, errorMessage);
            return a - b;
        }
    }

    
    function div(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a / b;
        }
    }

    
    function mod(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a % b;
        }
    }
}

interface IERC20 {
    
    function totalSupply() external view returns (uint256);

    
    function balanceOf(address account) external view returns (uint256);

    
    function transfer(address recipient, uint256 amount) external returns (bool);

    
    function allowance(address owner, address spender) external view returns (uint256);

    
    function approve(address spender, uint256 amount) external returns (bool);

    
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    
    event Transfer(address indexed from, address indexed to, uint256 value);

    
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IERC165 {
    
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

interface IERC721 is IERC165 {
    
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    
    function balanceOf(address owner) external view returns (uint256 balance);

    
    function ownerOf(uint256 tokenId) external view returns (address owner);

    
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    
    function approve(address to, uint256 tokenId) external;

    
    function getApproved(uint256 tokenId) external view returns (address operator);

    
    function setApprovalForAll(address operator, bool _approved) external;

    
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;
}

contract EthereanEmpire is Ownable {

using SafeMath for uint256;

event EmpireCreated(address owner, string name, string motto);
event EmpireCustomized(address owner, string newName, string newMotto);
event RewardClaimed(address claimer, uint amount);
event DropCreated(uint tokenId, string title, string description, string artist, uint cost, uint supply, uint end);
event DropMinted(address minter, uint dropId);


struct Empire {
    string name;
    string motto;
    bool exists;
}

struct Drop {
    uint tokenId;
    string title;
    string description;
    string artist;
    uint cost; 
    uint supply;
    uint end;
    bool exists;
    uint minted;
}

uint public ETHEREAN_MIN = 3;
uint public EMPIRE_EDIT_FEE = 200 ether;
uint public numDrops = 0;
address public ETHEREANS_CONTRACT_ADDRESS;
address public INVADER_CONTRACT_ADDRESS;
address public EMPIRE_DROPS_CONTRACT_ADDRESS;
uint constant public END_REWARDS = 1735693200; 
EthereansInterface private ethereanContract;
InvaderTokenInterface private invaderContract;
EmpireDropsInterface private empireDropsContract;

mapping(address => Empire) public empires;
address[] private empireAddresses;
mapping(uint => uint) public tokenToLastUpdated;
mapping(uint => Drop) public drops;
mapping(address => mapping(uint => bool)) public addressOwnsDrop;


constructor(address _ethereansAddress, address _invaderAddress) {
    ETHEREANS_CONTRACT_ADDRESS = _ethereansAddress;
    ethereanContract = EthereansInterface(_ethereansAddress);

    INVADER_CONTRACT_ADDRESS = _invaderAddress;
    invaderContract = InvaderTokenInterface(_invaderAddress);
}

function setEthereansContractAddress(address _contractAddress) external onlyOwner() {
    ETHEREANS_CONTRACT_ADDRESS = _contractAddress;
    ethereanContract = EthereansInterface(_contractAddress);
}

function setEmpireDropsContractAddress(address _contractAddress) external onlyOwner() {
    EMPIRE_DROPS_CONTRACT_ADDRESS = _contractAddress;
    empireDropsContract = EmpireDropsInterface(_contractAddress);
}


function newEmpire(string memory _name, string memory _motto) public {
    require(empires[msg.sender].exists == false, "Only one empire per wallet.");
    require(ethereanContract.balanceOf(msg.sender) >= ETHEREAN_MIN, "Did not meet minimum ethereans requirement.");
    empires[msg.sender] = Empire(_name, _motto, true);
    empireAddresses.push(msg.sender);
    emit EmpireCreated(msg.sender, _name, _motto);
}

function newDrop(uint _tokenId, string memory _title, string memory _description, string memory _artist, uint _cost, uint _supply, uint _end) external onlyOwner() {
    require(_tokenId >= 0, "Must supply a tokenId");
    require(_supply > 0, "Supply must be greater than 1.");
    require(block.timestamp < _end, "End date must be set in the future");
    numDrops = numDrops.add(1);
    drops[numDrops] = Drop(_tokenId, _title, _description, _artist, _cost, _supply, _end, true, 0);
    emit DropCreated(_tokenId, _title, _description, _artist, _cost, _supply, _end);
} 

function getEmpireAddresses() public view returns (address[] memory) {
    return empireAddresses;
}

function setEmpireEditFee(uint _newFee) external onlyOwner(){
    EMPIRE_EDIT_FEE = _newFee;
}

modifier hasEmpire {
    require(empires[msg.sender].exists == true, "Does not own empire.");
    _;
}

function customizeEmpire(string memory _newName, string memory _newMotto) external hasEmpire(){
    invaderContract.burnFrom(msg.sender, EMPIRE_EDIT_FEE);
    empires[msg.sender].name = _newName;
    empires[msg.sender].motto = _newMotto;
    emit EmpireCustomized(msg.sender, _newName, _newMotto);
}


function claimReward(uint[] memory _tokenIds) external hasEmpire() {
    uint ethereanBalance = ethereanContract.balanceOf(msg.sender);
    require(ethereanBalance >= ETHEREAN_MIN, "Did not meet minimum ethereans requirement to claim rewards.");
    uint currentTime = min(block.timestamp, END_REWARDS);
    uint totalElapsedTime;
    for (uint i=0; i<_tokenIds.length; i++) {
        uint tokenId = _tokenIds[i];
        uint lastUpdated = tokenToLastUpdated[tokenId];
        require(ethereanContract.ownerOf(tokenId) == msg.sender, "Etherean does not belong to you.");
        if (lastUpdated == 0) {
            tokenToLastUpdated[tokenId] = currentTime;
        } else if (lastUpdated > 0) {
            totalElapsedTime += currentTime.sub(lastUpdated);
            tokenToLastUpdated[tokenId] = currentTime;
        }
    }
    if (totalElapsedTime > 0) {
        uint multiplier = getMultiplier(ethereanBalance);
        uint rewardAmount = totalElapsedTime.mul(multiplier).mul(10**18).div(86400);
        invaderContract.claimReward(msg.sender, rewardAmount);
        emit RewardClaimed(msg.sender, rewardAmount);
    }
}

function mintDrop(uint _dropId) external hasEmpire(){
    Drop storage drop = drops[_dropId];
    require(drop.exists == true, "Drop does not exist.");
    require(block.timestamp < drop.end, "Drop has expired.");
    require(drop.minted < drop.supply, "Drop is sold out.");
    require(addressOwnsDrop[msg.sender][_dropId] == false, "Only one drop allowed per empire.");
    uint ethereanBalance = ethereanContract.balanceOf(msg.sender);
    require(ethereanBalance >= ETHEREAN_MIN, "Did not meet minimum ethereans requirement to mint drop.");
    if (drop.cost > 0) {
        invaderContract.burnFrom(msg.sender, drop.cost);
    }
    empireDropsContract.mint(msg.sender, drop.tokenId);
    drop.minted += 1;
    addressOwnsDrop[msg.sender][_dropId] = true;
    emit DropMinted(msg.sender, _dropId);
}

function min(uint a, uint b) internal pure returns (uint) {
		return a < b ? a : b;
	}


function getMultiplier(uint ethereanBalance) internal pure returns (uint) {
    if (ethereanBalance < 6) 
        return 10;
    if (ethereanBalance < 18)
        return 11;
    if (ethereanBalance < 72)
        return 12;
    return 13;
}

function withdraw() public onlyOwner() {
    uint balance = address(this).balance;
    payable(owner()).transfer(balance);
}

function recoverERC20(address _tokenAddress, uint _tokenAmount) public onlyOwner() {
    IERC20(_tokenAddress).transfer(owner(), _tokenAmount);
}

function recoverERC721(address _tokenAddress, uint _tokenId) public onlyOwner() {
    IERC721(_tokenAddress).safeTransferFrom(address(this), owner(), _tokenId);
}

}