contract CSCNFTFactory is ERC721Token, OperationalControl {

    /*** EVENTS ***/
    /// @dev The Created event is fired whenever a new asset comes into existence.
    event AssetCreated(address owner, uint256 assetId, uint256 assetType, uint256 sequenceId, uint256 creationTime);

    event DetachRequest(address owner, uint256 assetId, uint256 timestamp);

    event NFTDetached(address requester, uint256 assetId);

    event NFTAttached(address requester, uint256 assetId);

    // Mapping from assetId to uint encoded data for NFT
    mapping(uint256 => uint256) internal nftDataA;
    mapping(uint256 => uint128) internal nftDataB;

    // Mapping from Asset Types to count of that type in exsistance
    mapping(uint32 => uint64) internal assetTypeTotalCount;

    mapping(uint32 => uint64) internal assetTypeBurnedCount;
  
    // Mapping from index of a Asset Type to get AssetID
    mapping(uint256 => mapping(uint32 => uint64) ) internal sequenceIDToTypeForID;

     // Mapping from Asset Type to string name of type
    mapping(uint256 => string) internal assetTypeName;

    // Mapping from assetType to creation limit
    mapping(uint256 => uint32) internal assetTypeCreationLimit;

    // Indicates if attached system is Active (Transfers will be blocked if attached and active)
    bool public attachedSystemActive;

    // Is Asset Burning Active
    bool public canBurn;

    // Time LS Oracle has to respond to detach requests
    uint32 public detachmentTime = 300;

    /**
    * @dev Constructor function
    */
    constructor() public {
        require(msg.sender != address(0));
        paused = true;
        error = false;
        canBurn = false;
        managerPrimary = msg.sender;
        managerSecondary = msg.sender;
        bankManager = msg.sender;

        name_ = "CSCNFTFactory";
        symbol_ = "CSCNFT";
    }

    /**
    * @dev Checks msg.sender can transfer a token, by being owner, approved, or operator
    * @param _tokenId uint256 ID of the token to validate
    */
    modifier canTransfer(uint256 _tokenId) {
        uint256 isAttached = getIsNFTAttached(_tokenId);
        if(isAttached == 2) {
            //One-Time Auth for Physical Card Transfers
            require(msg.sender == managerPrimary ||
                msg.sender == managerSecondary ||
                msg.sender == bankManager ||
                otherManagers[msg.sender] == 1
            );
            updateIsAttached(_tokenId, 1);
        } else if(attachedSystemActive == true && isAttached >= 1) {
            require(msg.sender == managerPrimary ||
                msg.sender == managerSecondary ||
                msg.sender == bankManager ||
                otherManagers[msg.sender] == 1
            );
        }
        else {
            require(isApprovedOrOwner(msg.sender, _tokenId));
        }
        
    _;
    }

    /** Public Functions */

    // Returns the AssetID for the Nth assetID for a specific type
    function getAssetIDForTypeSequenceID(uint256 _seqId, uint256 _type) public view returns (uint256 _assetID) {
        return sequenceIDToTypeForID[_seqId][uint32(_type)];
    }

    function getAssetDetails(uint256 _assetId) public view returns(
        uint256 assetId,
        uint256 ownersIndex,
        uint256 assetTypeSeqId,
        uint256 assetType,
        uint256 createdTimestamp,
        uint256 isAttached,
        address creator,
        address owner
    ) {
        require(exists(_assetId));

        uint256 nftData = nftDataA[_assetId];
        uint256 nftDataBLocal = nftDataB[_assetId];

        assetId = _assetId;
        ownersIndex = ownedTokensIndex[_assetId];
        createdTimestamp = uint256(uint48(nftData>>160));
        assetType = uint256(uint32(nftData>>208));
        assetTypeSeqId = uint256(uint64(nftDataBLocal));
        isAttached = uint256(uint48(nftDataBLocal>>64));
        creator = address(nftData);
        owner = ownerOf(_assetId);
    }

    function totalSupplyOfType(uint256 _type) public view returns (uint256 _totalOfType) {
        return assetTypeTotalCount[uint32(_type)] - assetTypeBurnedCount[uint32(_type)];
    }

    function totalCreatedOfType(uint256 _type) public view returns (uint256 _totalOfType) {
        return assetTypeTotalCount[uint32(_type)];
    }

    function totalBurnedOfType(uint256 _type) public view returns (uint256 _totalOfType) {
        return assetTypeBurnedCount[uint32(_type)];
    }

    function getAssetRawMeta(uint256 _assetId) public view returns(
        uint256 dataA,
        uint128 dataB
    ) {
        require(exists(_assetId));

        dataA = nftDataA[_assetId];
        dataB = nftDataB[_assetId];
    }

    function getAssetIdItemType(uint256 _assetId) public view returns(
        uint256 assetType
    ) {
        require(exists(_assetId));
        uint256 dataA = nftDataA[_assetId];
        assetType = uint256(uint32(dataA>>208));
    }

    function getAssetIdTypeSequenceId(uint256 _assetId) public view returns(
        uint256 assetTypeSequenceId
    ) {
        require(exists(_assetId));
        uint256 dataB = nftDataB[_assetId];
        assetTypeSequenceId = uint256(uint64(dataB));
    }
    
    function getIsNFTAttached( uint256 _assetId) 
    public view returns(
        uint256 isAttached
    ) {
        uint256 nftData = nftDataB[_assetId];
        isAttached = uint256(uint48(nftData>>64));
    }

    function getAssetIdCreator(uint256 _assetId) public view returns(
        address creator
    ) {
        require(exists(_assetId));
        uint256 dataA = nftDataA[_assetId];
        creator = address(dataA);
    }

    function isAssetIdOwnerOrApproved(address requesterAddress, uint256 _assetId) public view returns(
        bool
    ) {
        return isApprovedOrOwner(requesterAddress, _assetId);
    }

    function getAssetIdOwner(uint256 _assetId) public view returns(
        address owner
    ) {
        require(exists(_assetId));

        owner = ownerOf(_assetId);
    }

    function getAssetIdOwnerIndex(uint256 _assetId) public view returns(
        uint256 ownerIndex
    ) {
        require(exists(_assetId));
        ownerIndex = ownedTokensIndex[_assetId];
    }

    /// @param _owner The owner whose ships tokens we are interested in.
    /// @dev This method MUST NEVER be called by smart contract code. First, it's fairly
    ///  expensive (it walks the entire NFT owners array looking for NFT belonging to owner),
    ///  but it also returns a dynamic array, which is only supported for web3 calls, and
    ///  not contract-to-contract calls.
    function tokensOfOwner(address _owner) external view returns(uint256[] ownerTokens) {
        uint256 tokenCount = balanceOf(_owner);

        if (tokenCount == 0) {
            // Return an empty array
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 resultIndex = 0;

            // We count on the fact that all Asset have IDs starting at 0 and increasing
            // sequentially up to the total count.
            uint256 _itemIndex;

            for (_itemIndex = 0; _itemIndex < tokenCount; _itemIndex++) {
                result[resultIndex] = tokenOfOwnerByIndex(_owner,_itemIndex);
                resultIndex++;
            }

            return result;
        }
    }

    // Get the name of the Asset type
    function getTypeName (uint32 _type) public returns(string) {
        return assetTypeName[_type];
    }


    /**
    * @dev Transfers the ownership of a given token ID to another address, modified to prevent transfer if attached and system is active
    */
    function transferFrom(
        address _from,
        address _to,
        uint256 _tokenId
    )
        public
        canTransfer(_tokenId)
    {
        require(_from != address(0));
        require(_to != address(0));

        clearApproval(_from, _tokenId);
        removeTokenFrom(_from, _tokenId);
        addTokenTo(_to, _tokenId);

        emit Transfer(_from, _to, _tokenId);
    }
    

    
    function multiBatchTransferFrom(
        uint256[] _assetIds, 
        address[] _fromB, 
        address[] _toB) 
        public
    {
        uint256 _id;
        address _to;
        address _from;
        
        for (uint256 i = 0; i < _assetIds.length; ++i) {
            _id = _assetIds[i];
            _to = _toB[i];
            _from = _fromB[i];

            require(isApprovedOrOwner(msg.sender, _id));

            require(_from != address(0));
            require(_to != address(0));
    
            clearApproval(_from, _id);
            removeTokenFrom(_from, _id);
            addTokenTo(_to, _id);
    
            emit Transfer(_from, _to, _id);
        }
        
    }
    
    function batchTransferFrom(uint256[] _assetIds, address _from, address _to) 
        public
    {
        uint256 _id;
        
        for (uint256 i = 0; i < _assetIds.length; ++i) {
            _id = _assetIds[i];

            require(isApprovedOrOwner(msg.sender, _id));

            require(_from != address(0));
            require(_to != address(0));
    
            clearApproval(_from, _id);
            removeTokenFrom(_from, _id);
            addTokenTo(_to, _id);
    
            emit Transfer(_from, _to, _id);
        }
    }
    
    function multiBatchSafeTransferFrom(
        uint256[] _assetIds, 
        address[] _fromB, 
        address[] _toB
        )
        public
    {
        uint256 _id;
        address _to;
        address _from;
        
        for (uint256 i = 0; i < _assetIds.length; ++i) {
            _id = _assetIds[i];
            _to  = _toB[i];
            _from  = _fromB[i];

            safeTransferFrom(_from, _to, _id);
        }
    }

    function batchSafeTransferFrom(
        uint256[] _assetIds, 
        address _from, 
        address _to
        )
        public
    {
        uint256 _id;
        for (uint256 i = 0; i < _assetIds.length; ++i) {
            _id = _assetIds[i];
            safeTransferFrom(_from, _to, _id);
        }
    }


    function batchApprove(
        uint256[] _assetIds, 
        address _spender
        )
        public
    {
        uint256 _id;
        for (uint256 i = 0; i < _assetIds.length; ++i) {
            _id = _assetIds[i];
            approve(_spender, _id);
        }
        
    }


    function batchSetApprovalForAll(
        address[] _spenders,
        bool _approved
        )
        public
    {
        address _spender;
        for (uint256 i = 0; i < _spenders.length; ++i) {
            _spender = _spenders[i];
            setApprovalForAll(_spender, _approved);
        }
    }  
    
    function requestDetachment(
        uint256 _tokenId
    )
        public
    {
        //Request can only be made by owner or approved address
        require(isApprovedOrOwner(msg.sender, _tokenId));

        uint256 isAttached = getIsNFTAttached(_tokenId);

        require(isAttached >= 1);

        if(attachedSystemActive == true) {
            //Checks to see if request was made and if time elapsed
            if(isAttached > 1 && block.timestamp - isAttached > detachmentTime) {
                isAttached = 0;
            } else if(isAttached > 1) {
                //Fail if time is already set for attachment
                require(isAttached == 1);
            } else {
                //Is attached, set detachment time and make request to detach
                emit DetachRequest(msg.sender, _tokenId, block.timestamp);
                isAttached = block.timestamp;
            }           
        } else {
            isAttached = 0;
        } 

        if(isAttached == 0) {
            emit NFTDetached(msg.sender, _tokenId);
        }

        updateIsAttached(_tokenId, isAttached);
    }

    function attachAsset(
        uint256 _tokenId
    )
        public
        canTransfer(_tokenId)
    {
        uint256 isAttached = getIsNFTAttached(_tokenId);

        require(isAttached == 0);
        isAttached = 1;

        updateIsAttached(_tokenId, isAttached);

        emit NFTAttached(msg.sender, _tokenId);
    }

    function batchAttachAssets(uint256[] _ids) public {
        for(uint i = 0; i < _ids.length; i++) {
            attachAsset(_ids[i]);
        }
    }

    function batchDetachAssets(uint256[] _ids) public {
        for(uint i = 0; i < _ids.length; i++) {
            requestDetachment(_ids[i]);
        }
    }

    function requestDetachmentOnPause (uint256 _tokenId) public 
    whenPaused {
        //Request can only be made by owner or approved address
        require(isApprovedOrOwner(msg.sender, _tokenId));

        updateIsAttached(_tokenId, 0);
    }

    function batchBurnAssets(uint256[] _assetIDs) public {
        uint256 _id;
        for(uint i = 0; i < _assetIDs.length; i++) {
            _id = _assetIDs[i];
            burnAsset(_id);
        }
    }

    function burnAsset(uint256 _assetID) public {
        // Is Burn Enabled
        require(canBurn == true);

        // Deny Action if Attached
        require(getIsNFTAttached(_assetID) == 0);

        require(isApprovedOrOwner(msg.sender, _assetID) == true);
        
        //Updates Type Total Count
        uint256 _assetType = getAssetIdItemType(_assetID);
        assetTypeBurnedCount[uint32(_assetType)] += 1;
        
        _burn(msg.sender, _assetID);
    }


    /** Dev Functions */

    function setTokenURIBase (string _tokenURI) public onlyManager {
        _setTokenURIBase(_tokenURI);
    }

    function setPermanentLimitForType (uint32 _type, uint256 _limit) public onlyManager {
        //Only allows Limit to be set once
        require(assetTypeCreationLimit[_type] == 0);

        assetTypeCreationLimit[_type] = uint32(_limit);
    }

    function setTypeName (uint32 _type, string _name) public anyOperator {
        assetTypeName[_type] = _name;
    }

    // Minting Function
    function batchSpawnAsset(address _to, uint256[] _assetTypes, uint256[] _assetIds, uint256 _isAttached) public anyOperator {
        uint256 _id;
        uint256 _assetType;
        for(uint i = 0; i < _assetIds.length; i++) {
            _id = _assetIds[i];
            _assetType = _assetTypes[i];
            _createAsset(_to, _assetType, _id, _isAttached, address(0));
        }
    }

    function batchSpawnAsset(address[] _toB, uint256[] _assetTypes, uint256[] _assetIds, uint256 _isAttached) public anyOperator {
        address _to;
        uint256 _id;
        uint256 _assetType;
        for(uint i = 0; i < _assetIds.length; i++) {
            _to = _toB[i];
            _id = _assetIds[i];
            _assetType = _assetTypes[i];
            _createAsset(_to, _assetType, _id, _isAttached, address(0));
        }
    }

    function batchSpawnAssetWithCreator(address[] _toB, uint256[] _assetTypes, uint256[] _assetIds, uint256[] _isAttacheds, address[] _creators) public anyOperator {
        address _to;
        address _creator;
        uint256 _id;
        uint256 _assetType;
        uint256 _isAttached;
        for(uint i = 0; i < _assetIds.length; i++) {
            _to = _toB[i];
            _id = _assetIds[i];
            _assetType = _assetTypes[i];
            _creator = _creators[i];
            _isAttached = _isAttacheds[i];
            _createAsset(_to, _assetType, _id, _isAttached, _creator);
        }
    }

    function spawnAsset(address _to, uint256 _assetType, uint256 _assetID, uint256 _isAttached) public anyOperator {
        _createAsset(_to, _assetType, _assetID, _isAttached, address(0));
    }

    function spawnAssetWithCreator(address _to, uint256 _assetType, uint256 _assetID, uint256 _isAttached, address _creator) public anyOperator {
        _createAsset(_to, _assetType, _assetID, _isAttached, _creator);
    }

    /// @dev Remove all Ether from the contract, shouldn't have any but just incase.
    function withdrawBalance() public onlyBanker {
        // We are using this boolean method to make sure that even if one fails it will still work
        bankManager.transfer(address(this).balance);
    }

    // Burn Functions

    function setCanBurn(bool _state) public onlyManager {
        canBurn = _state;
    }

    function burnAssetOperator(uint256 _assetID) public anyOperator {
        
        require(getIsNFTAttached(_assetID) > 0);

        //Updates Type Total Count
        uint256 _assetType = getAssetIdItemType(_assetID);
        assetTypeBurnedCount[uint32(_assetType)] += 1;
        
        _burn(ownerOf(_assetID), _assetID);
    }

    function toggleAttachedEnforement (bool _state) public onlyManager {
        attachedSystemActive = _state;
    }

    function setDetachmentTime (uint256 _time) public onlyManager {
        //Detactment Time can not be set greater than 2 weeks.
        require(_time <= 1209600);
        detachmentTime = uint32(_time);
    }

    function setNFTDetached(uint256 _assetID) public anyOperator {
        require(getIsNFTAttached(_assetID) > 0);

        updateIsAttached(_assetID, 0);
        emit NFTDetached(msg.sender, _assetID);
    }

    function setBatchDetachCollectibles(uint256[] _assetIds) public anyOperator {
        uint256 _id;
        for(uint i = 0; i < _assetIds.length; i++) {
            _id = _assetIds[i];
            setNFTDetached(_id);
        }
    }



    /** Internal Functions */

    // @dev For creating NFT Collectible
    function _createAsset(address _to, uint256 _assetType, uint256 _assetID, uint256 _attachState, address _creator) internal returns(uint256) {
        
        uint256 _sequenceId = uint256(assetTypeTotalCount[uint32(_assetType)]) + 1;

        //Will not allow creation if over limit
        require(assetTypeCreationLimit[uint32(_assetType)] == 0 || assetTypeCreationLimit[uint32(_assetType)] > _sequenceId);
        
        // These requires are not strictly necessary, our calling code should make
        // sure that these conditions are never broken.
        require(_sequenceId == uint256(uint64(_sequenceId)));

        //Creates NFT
        _mint(_to, _assetID);

        uint256 nftData = uint256(_creator); // 160 bit address of creator
        nftData |= now<<160; // 48 bit creation timestamp
        nftData |= _assetType<<208; // 32 bit item type 

        uint256 nftDataContinued = uint256(_sequenceId); // 64 bit sequence id of item
        nftDataContinued |= _attachState<<64; // 48 bit state and/or timestamp for detachment

        nftDataA[_assetID] = nftData;
        nftDataB[_assetID] = uint128(nftDataContinued);

        assetTypeTotalCount[uint32(_assetType)] += 1;
        sequenceIDToTypeForID[_sequenceId][uint32(_assetType)] = uint64(_assetID);

        // emit Created event
        emit AssetCreated(_to, _assetID, _assetType, _sequenceId, now);

        return _assetID;
    }

    function updateIsAttached(uint256 _assetID, uint256 _isAttached) 
    internal
    {
        uint256 nftData = nftDataB[_assetID];

        uint256 assetTypeSeqId = uint256(uint64(nftData));

        uint256 nftDataContinued = uint256(assetTypeSeqId); // 64 bit sequence id of item
        nftDataContinued |= _isAttached<<64; // 48 bit state and/or timestamp for detachment

        nftDataB[_assetID] = uint128(nftDataContinued);
    }



}
function withdrawBalance() public onlyBanker {
    // We are using this boolean method to make sure that even if one fails it will still work
    bankManager.transfer(address(this).balance);
}

/// @dev Remove all Ether from the contract, shouldn't have any but just incase.