// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23; //4; //23;

import "inetworkv1.sol"; // holds all interfaces
import "istandard.sol"; // all standard Interfaces
import "library.sol"; // Math, signedMath and Strings
import "abs_ownable.sol";

import "abs_metadatav1.sol"; 
import "abs_erc721.sol"; // main erc721
import "abs_payv2.sol"; // slight mod from v1

//
// Main class
//
contract SIMPV2 is PayV1, MetaDataV1,  ICountV1, IMintV1  {
    using Strings for uint256;

    uint8 private constant MINT_INDEX = 0;
    uint8 private constant BURN_INDEX = 1;
    uint8 private constant SENDLOCK_INDEX = 2;

    uint256 private m_totalSupply;
    uint256 private m_totalBurnt;
    // If m_limitedSupply is non-zero, this contract will toggle minting off as soon as the
    // limit is reached. Set in constructor.
    uint256 private m_limitedSupply; 

    address private m_participant; // must be participant in order to play
    // During mint, we call these two different contracts to make sure the two addresses that are provided
    // both hold ligitimate NFTs of the project. 
    address private m_agent = address(0);
    address private m_website = address(0);

    string private constant theSymbol = "SIMPV2";

    // the owner of this contract can assign token based metadata.
    mapping(uint256 tokenId => bool bOn) private m_bCustomURL;

    // used to enable and disable minting.
    bool private m_bMintToggle = true; // allows titled & owner to enable/disable minting.

    /**
     * @dev The constructor sets the initial state of the flip mapping, position zero is not used.
     * @param _theTitled This is the project owner's wallet. Considered the owner of the project
     * @param _thePennyOracle The Penny Oracle smart contract address
     */
    constructor(address _theTitled, 
                address _thePennyOracle, 
                address _theParticipant, 
                address _theAgent, 
                address _theWebsite,
                string memory _strProjectName,
                bool _bSoulbound,
                bool _bSendLock,
                uint256 _limit) 
        ERC721(_strProjectName, theSymbol, _bSoulbound, _bSendLock) 
        PayV1( _theTitled,  msg.sender, _thePennyOracle)
        {
            m_participant = _theParticipant;
            m_agent = _theAgent;
            m_website = _theWebsite;
            m_limitedSupply = _limit;
        }

    /**
     * @dev Used to set price and commission info. This should only be set by the Titled or
     * owner of the contract. This should be the first item set after launching the contract.
    */
    function payInitalize(uint256 _mintPennies, uint256 _timeoutDays, uint256 _minCount, uint256 _cutPercentAgent, uint256 _cutPercentWebsite) external onlyEither {
        require(iPayActive()==false,"Already setup");
        // validate that both counts add to less then 100
        require(((_cutPercentAgent+_cutPercentWebsite)<100),"bad math");
        iPayInitalize(_mintPennies, _timeoutDays, _minCount, _cutPercentAgent, _cutPercentWebsite);

        // Set the number of pennies for this other activity.
        iPayPenniesSet(BURN_INDEX, 0);
        iPayPenniesSet(SENDLOCK_INDEX, 10);
    }

    /**
     * @dev Used to set the penny price for minting the NFT. This should only be adjustable
     * by The Titled or the Owner of the contract.
     */
    function penniesSet(uint256 _mintPennies) public onlyEither {
        iPayPenniesSet(MINT_INDEX,_mintPennies);
    }
    /**
     * @dev Used to get the penny price for using this NFT.
     */
    function penniesRead() public view returns(uint256 mint) {
        mint = iPayPenniesRead(MINT_INDEX);
    }

    // If this contract is setup and ready for minting, this will return true.
    function IsReady() internal view returns(bool) {
        require(m_agent != address(0), "not setup");
        require(projectActive(),"Metadata not setup");
        require(true == m_bMintToggle,"mint disabled");
        return true;
    }

    // ------------------ IMintV1 functionality ------------------ 

    function imintable() internal view returns(bool bMintable) {
        // have we hit our mint limit? - no limit for this NFT
        if( m_limitedSupply > 0 ) {
            uint256 uiSupply = (m_totalSupply-m_totalBurnt);
            if(  uiSupply > m_limitedSupply ) {
                return false; // not mintable, we've hit our limit
            }
        }
        return m_bMintToggle; // return what the project owner wants.
    }

    function mintable() external view returns(bool bMintable)
    {
        return imintable();
    }
    function ownerLockToggle() external onlyEither {
        if( m_bMintToggle ) { m_bMintToggle = false; } else { m_bMintToggle = true; }
    }

    // ------------------ ERC721 functionality ------------------ 

    // Used when assigning a custom metadata file to a specific token id.
    function customMetadataToggle(uint256 _tokenId) external onlyEither {
        if( true == m_bCustomURL[_tokenId] ) {
            m_bCustomURL[_tokenId] = false;
        } else {
            m_bCustomURL[_tokenId] = true;
        }
    }
    
    /**
     * @dev returns the appropriate metadata URI for the given token id. Note that the token id
     * must exist in order for it to be built and returned. 
     */
    function tokenURI(uint256 _tokenId) public view override returns (string memory) {
        _requireOwned(_tokenId);

        string memory projectName;
        (, projectName, ) = projectDataCurrent();

        string memory resultURI;
        if( true == m_bCustomURL[_tokenId] ) {
            resultURI = string.concat(_baseURI(), projectName, "_", Strings.toString(_tokenId), ".json");
        } else {
            resultURI = string.concat(_baseURI(), projectName, ".json");
        }
        return resultURI;
    }

    /**
     * @dev Base URI for computing {tokenURI}. 
     */
    function _baseURI() internal view override(ERC721) returns (string memory) {
        string memory resultURI = string.concat(projectBaseURI(),"metadata/");
        return resultURI;
    }

    // In order to mint, we have to make that functionality public. We will do so with a
    // function called payMint() that is payable. If the checks are passed, we will call the
    // safeMint override in order to track the total minting. 
    function safeMint(address to, address _founder, address _partner, address _website) internal returns(uint256 tokenId) {
        m_totalSupply++; // used to id the tokens. 

        mintSplit(_founder, _partner, _website);

        _safeMint(to, m_totalSupply);
        m_bCustomURL[m_totalSupply]=false; // no custom metadata at start
        return m_totalSupply;
    }
    // This function is called by the owner to mint non-commission tokens
    function safeOwnerMint(address to) internal returns(uint256 tokenId) {
        m_totalSupply++; // used to id the tokens. 

        _safeMint(to, m_totalSupply);
        m_bCustomURL[m_totalSupply]=false; // no custom metadata at start
        return m_totalSupply;
    }

    //  ------------------ Used for IPayV1 Interface  ------------------ 

    function payMintPrice() external view returns(uint256 mintWei) {
        return ipayPrice(MINT_INDEX);
    }

    /**
     * @dev Anyone is allowed to mint provided the metadata information has already been
     * setup, the right amount of coin is provided. There is no limit to the 
     * number of NFTs that can be minted. The caller must provide two wallets. Both 
     * wallets must hold agent NFTs
     */
    function payMint(uint256 _AgentId, uint256 _websiteId) public payable returns(uint256 tokenId) {

        require(IsReady(),"Not setup yet");

        require(imintable()==true,"not mintable"); // Is this a limited mint?

        require(ipayValidateAmount(MINT_INDEX), "Wrong amount sent");
        require( IERC721(m_participant).balanceOf(msg.sender) > 0, "must hold participant to play");

        // Validate that the minting code is crediting a website NFT holder
        address cutWebsiteAddr = IERC721(m_website).ownerOf(_websiteId);
        require(cutWebsiteAddr != address(0),"invalid website Id");

        // now resolve the agent id
        (address cutFounderAddr, address cutPartnerAddr) = IRegisterV1(m_agent).regResolve(_AgentId);
        require(cutFounderAddr != address(0),"Invalid agent Id");
        require(cutPartnerAddr != address(0),"Invalid agent Id");

        // Any other restrictions on minting? 
        return safeMint(msg.sender, cutFounderAddr, cutPartnerAddr, cutWebsiteAddr);
    }

    // This routine will be called by either the contract owner or The Titled in order to 
    // capture the outstanding commission from a deliquent agent. Note that the funds are
    // assigned to The Titled. 
    function paySweep(address _anAccount) external onlyEither returns(bool) {
        
        require(sweepConditions(_anAccount), "account not ready");
        return updateAbandonedAccount(_anAccount);
    }

    // There are only two ways to withdraw and they both come through here.
    function iWithdraw(address _anAccount) internal returns(bool bSuccess) {

        uint256 uiWei = claimAccount(_anAccount);
        payable(_anAccount).transfer(uiWei);
        return true;
    }
    /**
     * @dev If you have an account, you can get to your funds if all conditions are met
     */
    function payWithdraw() external returns(bool) {

        // has to pass the checks in order to withdraw.
        address recieverAddr = msg.sender;
        require(claimConditions(recieverAddr),"conditions not met");
        return iWithdraw(recieverAddr);
    }
    function payWithdrawTitled() external onlyEither returns(bool) {

        // has to pass the checks in order to withdraw.
        address recieverAddr = titled();
        require(claimConditions(recieverAddr),"conditions not met");
        return iWithdraw(recieverAddr);
    }

    //  ------------------  IMetadataV1 interface ------------------ 

    /**
     * @dev This routine sets the initial state for the project metadata. The _URI is used
     * as the base URI for all token URIs. The _Project name represents the file name(without
     * extension) on the server pointed to by _URI. The _Project JSON file can be hashed to
     * produce the SHA1 hash value represented by _hash. Only the owner of the contract can
     * set the project location. If the project moves, the latest addition will be considered
     * the working location.
     */
    function projectUpdate(string calldata _URI, string calldata _Project, string calldata _hash) public onlyOwner {
        require(bytes(_URI).length > 0,"Project base URI needs valid path");
        require(bytes(_Project).length > 0,"Project name length invalid");
        require(bytes(_hash).length == 40,"requires SHA1 hash string");
        return iProjectUpdate(_URI,_Project,_hash);
    }

    //  ------------------  IPennyOracleV1 interface ------------------ 
    //
    // The Penny Oracle functionality is handled by the IPay class.
    //
    // This NFT uses the Penny Oracle. It is set in the constructor, but if it changes
    // we can update that source here.
    //
    function pennyOracleSet(address _PennyOracle) public onlyOwner {
        require(_PennyOracle != address(0), "No Penny to Reference");
        require(IERC165(_PennyOracle).supportsInterface(type(IPennyOracleV1).interfaceId),"doesn't support IPennyOracleV1");
        iPennyOracleSet(_PennyOracle);
    }

    //  ------------------  ICountV1 interface ------------------ 

    /**
     * @dev Anyone is allowed to see the totals
     */
    function totalSupply() external view returns(uint256 theTotalSupply) {
        return m_totalSupply-m_totalBurnt;
    }


    //  ------------------  IERC165 interface ------------------ 
    //
    // When this NFT project is added to The Gallery V1, that contract will query
    // to make sure this NFT supports IMetadataV1 and IPayV1. It will not accept
    // NFT projects that don't offer this functionality.
    //
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IPennyOracleV1).interfaceId ||
            interfaceId == type(IMetadataV1).interfaceId ||
            interfaceId == type(IPayV1).interfaceId ||
            interfaceId == type(IMintV1).interfaceId ||
            interfaceId == type(ICountV1).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    // ------------------------ IBurnV1 functionality ----------------------------------

    // returns the cost of burning.
    function payBurnPrice() external view returns(uint256 burnWei) {
        return ipayPrice(BURN_INDEX);
    }
    // if the caller passes in zero, we'll validate that this token is soulbound and fetch
    // the token Id that needs to be burnt. 
    function payBurn(uint256 _tokenId) external payable {

        require(ipayValidateAmount(BURN_INDEX), "Wrong amount sent"); // must send the correct amount to burn.
        if(  0 == _tokenId) {
            require(IsSoulBound(),"must be soulBound");
            _tokenId = theTokenId(msg.sender);
            _requireOwned(_tokenId);
            require(ownerOf(_tokenId)==msg.sender,"Not setup yet"); // only holder can burn the token
        } else {
            // not soulbound, we have to check that the caller owns this specific token
            _requireOwned(_tokenId);
            require(ownerOf(_tokenId)==msg.sender,"Not setup yet"); // only holder can burn the token
        }
        // record the payment for The Titled
        titledPayment();
        _burn(_tokenId);
        m_totalBurnt++;
    }

    // ------------------------ sendLock functionality ----------------------------------

    function paySendLockPrice() external view returns(uint256 sendLockWei) {
        return ipayPrice(SENDLOCK_INDEX);
    }

    // There is a cost for using sendlock functionality.
    function sendLockRegister(address to, uint256 tokenId) public payable {
        _requireOwned(tokenId);
        require(IsSoulBound()==false,"soulbound");
        require(_ownerOf(tokenId) == msg.sender, "Must own token");
        require(to != msg.sender, "Don't send to self");
        require(address(0) != to, "must provide valid destination");
        require(ipayPrice(SENDLOCK_INDEX) == msg.value, "Wrong amount sent");

        // record the payment for The Titled
        titledPayment();
        _sendLockRegister(to,tokenId);
    }

    // ------------------------ special mint function ----------------------------------

    // routine that allows owner to mint without commissions and even after the mint limit
    // has been reached. 
    function ownerMint(address _recipient) external onlyEither returns(uint256 tokenId) {
        require(IsReady(),"Not setup yet");
        return safeOwnerMint(_recipient);
    }

}
