contract ShardGovernor is IERC721Receiver {

  using SafeMath for uint256;

	// Equals `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
	bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;

	IShardRegistry private _shardRegistry;
	IShardOffering private _shardOffering;
	ShotgunClause private _currentShotgunClause;
	address payable private _offererAddress;
	address[] private _nftRegistryAddresses;
	address payable private _niftexWalletAddress;
	address payable private _artistWalletAddress;
	uint256[] private _tokenIds;

	enum ClaimWinner { None, Claimant, Counterclaimant }
	address[] private _shotgunAddressArray;
	mapping(address => uint) private _shotgunMapping;
	uint private _shotgunCounter;

	event NewShotgun(address indexed shotgun);
	event ShardsClaimed(address indexed claimant, uint indexed shardAmount);
	event NftRedeemed(address indexed redeemer);
	event ShotgunEnacted(address indexed enactor);
	event ShardsCollected(address indexed collector, uint indexed shardAmount, address indexed shotgun);

	/**
		* @dev Checks whether offerer indeed owns the relevant NFT.
		* @dev Offering deadline starts ticking on deployment, but offerer needs to transfer
		* NFT to this contract before anyone can contribute.
		*/
  constructor(
  	address[] memory nftRegistryAddresses,
  	address payable offererAddress,
  	uint256[] memory tokenIds,
  	address payable niftexWalletAddress,
  	address payable artistWalletAddress
	) public {
		for (uint x = 0; x < tokenIds.length; x++) {
			require(
				IERC721(nftRegistryAddresses[x]).ownerOf(tokenIds[x]) == offererAddress,
				"Offerer is not owner of tokenId"
			);
		}

		_nftRegistryAddresses = nftRegistryAddresses;
		_niftexWalletAddress = niftexWalletAddress;
		_artistWalletAddress = artistWalletAddress;
		_tokenIds = tokenIds;
		_offererAddress = offererAddress;
	}

	/**
		* @dev Used to receive ether from the pullLiquidity function.
		*/
	function() external payable { }

	function deploySubcontracts(
		uint liqProviderCutInShards,
		uint artistCutInShards,
		uint pricePerShardInWei,
		uint shardAmountOffered,
		uint offeringDeadline,
		uint256 cap,
		string calldata name,
		string calldata symbol,
		bool shotgunDisabled,
		address factoryAddress
	) external {
		require(
			msg.sender == _niftexWalletAddress,
			"[deploySubcontracts] Unauthorized call"
		);

		require(
			address(_shardRegistry) == address(0) && address(_shardOffering) == address(0),
			"[deploySubcontracts] Contract(s) exist"
		);

		IFactory factory = IFactory(factoryAddress);
		(_shardRegistry, _shardOffering) = factory.newSet(
			liqProviderCutInShards,
			artistCutInShards,
			pricePerShardInWei,
			shardAmountOffered,
			offeringDeadline,
			cap,
			name,
			symbol,
			shotgunDisabled
		);
	}

	/**
		* @notice Issues Shards upon completion of Offering.
		* @dev Cap should equal totalSupply when all Shards have been claimed.
		* @dev The Offerer may close an undersubscribed Offering once the deadline has
		* passed and claim the remaining Shards.
		*/
	function checkOfferingAndIssue() external {
		require(
			_shardRegistry.totalSupply() != _shardRegistry.cap(),
			"[checkOfferingAndIssue] Shards have already been issued"
		);
		require(
			!_shardOffering.hasClaimedShards(msg.sender),
			"[checkOfferingAndIssue] You have already claimed your Shards"
		);
		require(
			_shardOffering.offeringCompleted() ||
			(now > _shardOffering.offeringDeadline() && !_shardOffering.offeringCompleted()),
			"Offering not completed or deadline not expired"
		);
		if (_shardOffering.offeringCompleted()) {
			if (_shardOffering.getSubEther(msg.sender) != 0) {
				_shardOffering.claimShards(msg.sender);
				uint subShards = _shardOffering.getSubShards(msg.sender);
				bool success = _shardRegistry.mint(msg.sender, subShards);
				require(success, "[checkOfferingAndIssue] Mint failed");
				emit ShardsClaimed(msg.sender, subShards);
			} else if (msg.sender == _offererAddress) {
				_shardOffering.claimShards(msg.sender);
				uint offShards = _shardOffering.offererShardAmount();
				bool success = _shardRegistry.mint(msg.sender, offShards);
				require(success, "[checkOfferingAndIssue] Mint failed");
				emit ShardsClaimed(msg.sender, offShards);
			}
		} else {
			_shardOffering.wrapUpOffering();
			uint remainingShards = _shardRegistry.cap().sub(_shardOffering.totalShardsClaimed());
			remainingShards = remainingShards
				.sub(_shardOffering.liqProviderCutInShards())
				.sub(_shardOffering.artistCutInShards());
			bool success = _shardRegistry.mint(_offererAddress, remainingShards);
			require(success, "[checkOfferingAndIssue] Mint failed");
			emit ShardsClaimed(msg.sender, remainingShards);
		}
	}

	/**
		* @notice Used by NIFTEX to claim predetermined amount of shards in offering in order
		* to bootstrap liquidity on Uniswap-type exchange.
		*/
	/* function claimLiqProviderShards() external {
		require(
			msg.sender == _niftexWalletAddress,
			"[claimLiqProviderShards] Unauthorized caller"
		);
		require(
			!_shardOffering.hasClaimedShards(msg.sender),
			"[claimLiqProviderShards] You have already claimed your Shards"
		);
		require(
			_shardOffering.offeringCompleted(),
			"[claimLiqProviderShards] Offering not completed"
		);
		_shardOffering.claimShards(_niftexWalletAddress);
		uint cut = _shardOffering.liqProviderCutInShards();
		bool success = _shardRegistry.mint(_niftexWalletAddress, cut);
		require(success, "[claimLiqProviderShards] Mint failed");
		emit ShardsClaimed(msg.sender, cut);
	} */

	function mintReservedShards(address _beneficiary) external {
		bool niftex;
		if (_beneficiary == _niftexWalletAddress) niftex = true;
		require(
			niftex ||
			_beneficiary == _artistWalletAddress,
			"[mintReservedShards] Unauthorized beneficiary"
		);
		require(
			!_shardOffering.hasClaimedShards(_beneficiary),
			"[mintReservedShards] Shards already claimed"
		);
		_shardOffering.claimShards(_beneficiary);
		uint cut;
		if (niftex) {
			cut = _shardOffering.liqProviderCutInShards();
		} else {
			cut = _shardOffering.artistCutInShards();
		}
		bool success = _shardRegistry.mint(_beneficiary, cut);
		require(success, "[mintReservedShards] Mint failed");
		emit ShardsClaimed(_beneficiary, cut);
	}

	/**
		* @notice In the unlikely case that one account accumulates all Shards,
		* they can be redeemed directly for the underlying NFT.
		*/
	function redeem() external {
		require(
			_shardRegistry.balanceOf(msg.sender) == _shardRegistry.cap(),
			"[redeem] Account does not own total amount of Shards outstanding"
		);
		// Disable contracts to improve compatibility with certain NFT projects
		require(
			msg.sender == tx.origin,
			"[redeem] Caller must be wallet"
		);
		for (uint x = 0; x < _tokenIds.length; x++) {
			IERC721(_nftRegistryAddresses[x]).safeTransferFrom(address(this), msg.sender, _tokenIds[x]);
		}
		emit NftRedeemed(msg.sender);
	}

	/**
		* @notice Creates a new Shotgun claim.
		* @dev This Function is called from the Shard Registry because the claimant's
		* Shards must be frozen until the Shotgun is resolved: if they lose the claim,
		* their Shards are automatically distributed to the counterclaimants.
		* @dev The Registry is paused while an active Shotgun claim exists to
		* let the process work in an orderly manner.
		* @param initialClaimantAddress wallet address of the person who initiated Shotgun.
		* @param initialClaimantBalance Shard balance of the person who initiated Shotgun.
		*/
	function claimInitialShotgun(
		address payable initialClaimantAddress,
		uint initialClaimantBalance
	) external payable returns (bool) {
		require(
			msg.sender == address(_shardRegistry),
			"[claimInitialShotgun] Caller not authorized"
		);
		_currentShotgunClause = (new ShotgunClause).value(msg.value)(
			initialClaimantAddress,
			initialClaimantBalance,
			address(_shardRegistry)
		);
		emit NewShotgun(address(_currentShotgunClause));
		_shardRegistry.pause();
		_shotgunAddressArray.push(address(_currentShotgunClause));
		_shotgunCounter++;
		_shotgunMapping[address(_currentShotgunClause)] = _shotgunCounter;
		return true;
	}

	/**
		* @notice Effects the results of a (un)successful Shotgun claim.
		* @dev This Function can only be called by a Shotgun contract in two scenarios:
		* - Counterclaimants raise enough ether to buy claimant out
		* - Shotgun deadline passes without successful counter-raise, claimant wins
		*/
	function enactShotgun() external {
		require(
			_shotgunMapping[msg.sender] != 0,
			"[enactShotgun] Invalid Shotgun Clause"
		);
		ShotgunClause _shotgunClause = ShotgunClause(msg.sender);
		address initialClaimantAddress = _shotgunClause.initialClaimantAddress();
		if (uint(_shotgunClause.claimWinner()) == uint(ClaimWinner.Claimant)) {
			_shardRegistry.burn(_shardRegistry.balanceOf(initialClaimantAddress));
			for (uint x = 0; x < _tokenIds.length; x++) {
				IERC721(_nftRegistryAddresses[x]).safeTransferFrom(address(this), initialClaimantAddress, _tokenIds[x]);
			}
			_shardRegistry.unpause();
			emit ShotgunEnacted(address(_shotgunClause));
		} else if (uint(_shotgunClause.claimWinner()) == uint(ClaimWinner.Counterclaimant)) {
			_shardRegistry.unpause();
			emit ShotgunEnacted(address(_shotgunClause));
		}
	}

	/**
		* @notice Transfer Shards to counterclaimants after unsuccessful Shotgun claim.
		* @dev This contract custodies the claimant's Shards when they claim Shotgun -
		* if they lose the claim these Shards must be transferred to counterclaimants.
		* This process is initiated by the relevant Shotgun contract.
		* @param recipient wallet address of the person receiving the Shards.
		* @param amount the amount of Shards to receive.
		*/
	function transferShards(address recipient, uint amount) external {
		require(
			_shotgunMapping[msg.sender] != 0,
			"[transferShards] Unauthorized caller"
		);
		bool success = _shardRegistry.transfer(recipient, amount);
		require(success, "[transferShards] Transfer failed");
		emit ShardsCollected(recipient, amount, msg.sender);
	}

	/**
		* @notice Allows liquidity providers to pull funds during shotgun.
		* @dev Requires Unitokens to be sent to the contract so the contract can
		* remove liquidity.
		* @param exchangeAddress address of the Uniswap pool.
		* @param liqProvAddress address of the liquidity provider.
		* @param uniTokenAmount liquidity tokens to redeem.
		* @param minEth minimum ether to withdraw.
		* @param minTokens minimum tokens to withdraw.
		* @param deadline deadline for the withdrawal.
		*/
	function pullLiquidity(
		address exchangeAddress,
		address liqProvAddress,
		uint256 uniTokenAmount,
		uint256 minEth,
		uint256 minTokens,
		uint256 deadline
	) public {
		require(msg.sender == _niftexWalletAddress, "[pullLiquidity] Unauthorized call");
		IUniswapExchange uniExchange = IUniswapExchange(exchangeAddress);
		uniExchange.transferFrom(liqProvAddress, address(this), uniTokenAmount);
		_shardRegistry.unpause();
		(uint ethAmount, uint tokenAmount) = uniExchange.removeLiquidity(uniTokenAmount, minEth, minTokens, deadline);
		(bool ethSuccess, ) = liqProvAddress.call.value(ethAmount)("");
		require(ethSuccess, "[pullLiquidity] ETH transfer failed.");
		bool tokenSuccess = _shardRegistry.transfer(liqProvAddress, tokenAmount);
		require(tokenSuccess, "[pullLiquidity] Token transfer failed");
		_shardRegistry.pause();
	}

	/**
		* @dev Utility function to check if a Shotgun is in progress.
		*/
	function checkShotgunState() external view returns (bool) {
		if (_shotgunCounter == 0) {
			return true;
		} else {
			ShotgunClause _shotgunClause = ShotgunClause(_shotgunAddressArray[_shotgunCounter - 1]);
			if (_shotgunClause.shotgunEnacted()) {
				return true;
			} else {
				return false;
			}
		}
	}

	function currentShotgunClause() external view returns (address) {
		return address(_currentShotgunClause);
	}

	function shardRegistryAddress() external view returns (address) {
		return address(_shardRegistry);
	}

	function shardOfferingAddress() external view returns (address) {
		return address(_shardOffering);
	}

	function getContractBalance() external view returns (uint) {
		return address(this).balance;
	}

	function offererAddress() external view returns (address payable) {
		return _offererAddress;
	}

	function shotgunCounter() external view returns (uint) {
		return _shotgunCounter;
	}

	function shotgunAddressArray() external view returns (address[] memory) {
		return _shotgunAddressArray;
	}

	function getNftRegistryAddresses() external view returns (address[] memory) {
		return _nftRegistryAddresses;
	}

	function getNftTokenIds() external view returns (uint256[] memory) {
		return _tokenIds;
	}

	function getOwner() external view returns (address) {
		return _niftexWalletAddress;
	}

	/**
		* @dev Utility function to check whether this contract owns the Sharded NFT.
		*/
	function checkLock() external view returns (bool) {
		if (address(_shardOffering) == address(0) || address(_shardRegistry) == address(0)) return false;

		for (uint x = 0; x < _tokenIds.length; x++) {
			address owner = IERC721(_nftRegistryAddresses[x]).ownerOf(_tokenIds[x]);
			if (owner != address(this)) {
				return false;
			}
		}

		return true;
	}

	/**
		* @notice Handle the receipt of an NFT.
		* @dev The ERC721 smart contract calls this function on the recipient
		* after a `safetransfer`. This function MAY throw to revert and reject the
		* transfer. Return of other than the magic value MUST result in the
		* transaction being reverted.
		* Note: the contract address is always the message sender.
		* @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
		*/
	function onERC721Received(address, address, uint256, bytes memory) public returns(bytes4) {
		return _ERC721_RECEIVED;
	}
}
/**
 	* @title Contract managing Shard lifecycle (NFT custody + Shard issuance and redemption)
 	* @author Joel Hubert (Metalith.io)
 	* @dev OpenZeppelin contracts are not ready for 0.6.0 yet, using 0.5.15.
 	* @dev This contract owns the Registry, Offering and any Shotgun contracts,
 	* making it the gateway for core state changes.
 	*/
function mintReservedShards(address _beneficiary) external {
	bool niftex;
	if (_beneficiary == _niftexWalletAddress) niftex = true;
	require(
		niftex ||
		_beneficiary == _artistWalletAddress,
		"[mintReservedShards] Unauthorized beneficiary"
	);
	require(
		!_shardOffering.hasClaimedShards(_beneficiary),
		"[mintReservedShards] Shards already claimed"
	);
	_shardOffering.claimShards(_beneficiary);
	uint cut;
	if (niftex) {
		cut = _shardOffering.liqProviderCutInShards();
	} else {
		cut = _shardOffering.artistCutInShards();
	}
	bool success = _shardRegistry.mint(_beneficiary, cut);
	require(success, "[mintReservedShards] Mint failed");
	emit ShardsClaimed(_beneficiary, cut);
}

/* function claimLiqProviderShards() external {
 		require(
 			msg.sender == _niftexWalletAddress,
 			"[claimLiqProviderShards] Unauthorized caller"
 		);
 		require(
 			!_shardOffering.hasClaimedShards(msg.sender),
 			"[claimLiqProviderShards] You have already claimed your Shards"
 		);
 		require(
 			_shardOffering.offeringCompleted(),
 			"[claimLiqProviderShards] Offering not completed"
 		);
 		_shardOffering.claimShards(_niftexWalletAddress);
 		uint cut = _shardOffering.liqProviderCutInShards();
 		bool success = _shardRegistry.mint(_niftexWalletAddress, cut);
 		require(success, "[claimLiqProviderShards] Mint failed");
 		emit ShardsClaimed(msg.sender, cut);
 	} */