// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// This is a special "passive" migration contract of MTM. 
// Every single method is modified and has custom "passive" migration proxy logic.

abstract contract Ownable {
    address public owner;
    constructor() { owner = msg.sender; }
    modifier onlyOwner { require(owner == msg.sender, "Ownable: NO"); _; }
    function transferOwnership(address newOwner_) public virtual onlyOwner {
        owner = newOwner_; 
    }
}

interface iCM {
    // Views
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function totalSupply() external view returns (uint256);
    function ownerOf(uint256 tokenId_) external view returns (address);
    function balanceOf(address address_) external view returns (uint256);
    function getApproved(uint256 tokenId_) external view returns (address);
    function isApprovedForAll(address owner_, address operator_) external view returns (bool);
}

// ERC721I Functions, but we modified it for passive migration method
// ERC721IMigrator uses local state storage for gas savings.
// It is like ERC721IStorage and ERC721IOperator combined into one.
contract ERC721IMigrator is Ownable {

    // Interface the MTM Characters Main V1
    iCM public CM;
    function setCM(address address_) external onlyOwner {
        CM = iCM(address_);
    }

    // Name and Symbol Stuff
    string public name; string public symbol;
    constructor(string memory name_, string memory symbol_) {
        name = name_; symbol = symbol_;
    }

    // We turned these to _ prefix so we can use a override function
    // To display custom proxy and passive migration logic
    uint256 public totalSupply;
    mapping(uint256 => address) public _ownerOf;
    mapping(address => uint256) public _balanceOf;

    // Here we have to keep track of a initialized balanceOf to prevent any view issues
    mapping(address => bool) public _balanceOfInitialized;

    
    // We disregard the previous contract's approvals
    mapping(uint256 => address) public getApproved;
    mapping(address => mapping(address => bool)) public isApprovedForAll;

    // // TotalSupply Setter
    // Here, we set the totalSupply to equal the previous
    function setTotalSupply(uint256 totalSupply_) external onlyOwner {
        totalSupply = totalSupply_; 
    }

    // // Initializer
    // This is a custom Transfer emitter for the initialize of this contract only
    function initialize(uint256[] calldata tokenIds_, address[] calldata owners_) external onlyOwner {
        require(tokenIds_.length == owners_.length,
            "initialize(): array length mismatch!");
        
        for (uint256 i = 0; i < tokenIds_.length; i++) {
            emit Transfer(address(0x0), owners_[i], tokenIds_[i]);
        }
    }

    // OwnerOf (Proxy View)
    function ownerOf(uint256 tokenId_) public view returns (address) {
        // Find out of the _ownerOf slot has been initialized.
        // We hardcode the tokenId_ to save gas.
        if (tokenId_ <= 3259 && _ownerOf[tokenId_] == address(0x0)) {
            // _ownerOf[tokenId_] is not initialized yet, so return the CM V1 value.
            return CM.ownerOf(tokenId_);
        } else {
            // If it is already initialized, or is higher than migration Id
            // return local state storage instead.
            return _ownerOf[tokenId_];
        }
    }

    // BalanceOf (Proxy View)
    function balanceOf(address address_) public view returns (uint256) {
        // Proxy the balance function
        // We have a tracker of initialization of _balanceOf to track the differences
        // If initialized, we use the state storage. Otherwise, we use CM V1 storage.
        if (_balanceOfInitialized[address_]) {
            return _balanceOf[address_]; 
        } else {
            return CM.balanceOf(address_);
        }
    }

    // Events! L[o_o]⅃ 
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Mint(address indexed to, uint256 tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    // Functions
    function _mint(address to_, uint256 tokenId_) internal virtual {
        require(to_ != address(0x0), "ERC721IMigrator: _mint() Mint to Zero Address!");
        require(ownerOf(tokenId_) == address(0x0), "ERC721IMigrator: _mint() Token already Exists!");

        // // ERC721I Logic

        // We set _ownerOf in a normal way
        _ownerOf[tokenId_] = to_;

        // We rebalance the _balanceOf on initialization, otherwise follow normal ERC721I logic
        if (_balanceOfInitialized[to_]) {
            // If we are already initialized
            _balanceOf[to_]++;
        } else {
            _balanceOf[to_] = (CM.balanceOf(to_) + 1);
            _balanceOfInitialized[to_] = true;
        }

        // Increment TotalSupply as normal
        totalSupply++;

        // // ERC721I Logic End

        // Emit Events
        emit Transfer(address(0x0), to_, tokenId_);
        emit Mint(to_, tokenId_);
    }

    function _transfer(address from_, address to_, uint256 tokenId_) internal virtual {
        require(from_ == ownerOf(tokenId_), "ERC721IMigrator: _transfer() Transfer from_ != ownerOf(tokenId_)");
        require(to_ != address(0x0), "ERC721IMigrator: _transfer() Transfer to Zero Address!");

        // // ERC721I Transfer Logic

        // If token has an approval
        if (getApproved[tokenId_] != address(0x0)) {
            // Remove the outstanding approval
            getApproved[tokenId_] = address(0x0);
        }

        // Set the _ownerOf to the receiver
        _ownerOf[tokenId_] = to_;

        // // Initialize and Rebalance _balanceOf 
        if (_balanceOfInitialized[from_]) {
            // If from_ is initialized, do normal balance change
            _balanceOf[from_]--;
        } else {
            // If from_ is NOT initialized, follow rebalance flow
            _balanceOf[from_] = (CM.balanceOf(from_) - 1);
            // Set from_ as initialized
            _balanceOfInitialized[from_] = true;
        }

        if (_balanceOfInitialized[to_]) {
            // If to_ is initialized, do normal balance change
            _balanceOf[to_]++;
        } else {
            // If to_ is NOT initialized, follow rebalance flow
            _balanceOf[to_] = (CM.balanceOf(to_) + 1);
            // Set to_ as initialized;
            _balanceOfInitialized[to_] = true;
        }

        // // ERC721I Transfer Logic End

        emit Transfer(from_, to_, tokenId_);
    }

    // Approvals
    function _approve(address to_, uint256 tokenId_) internal virtual {
        if (getApproved[tokenId_] != to_) {
            getApproved[tokenId_] = to_;
            emit Approval(ownerOf(tokenId_), to_, tokenId_);
        }
    }
    function _setApprovalForAll(address owner_, address operator_, bool approved_) internal virtual {
        require(owner_ != operator_, "ERC721IMigrator: _setApprovalForAll() Owner must not be the Operator!");
        isApprovedForAll[owner_][operator_] = approved_;
        emit ApprovalForAll(owner_, operator_, approved_);
    }

    // // Functional Internal Views
    function _isApprovedOrOwner(address spender_, uint256 tokenId_) internal view returns (bool) {
        address _owner = ownerOf(tokenId_);
        require(_owner != address(0x0), "ERC721IMigrator: _isApprovedOrOwner() Owner is Zero Address!");
        return (spender_ == _owner // is the owner OR
            || spender_ == getApproved[tokenId_] // is approved for token OR
            || isApprovedForAll[_owner][spender_] // isApprovedForAll spender 
        );
    }

    // Exists
    function _exists(uint256 tokenId_) internal view virtual returns (bool) {
        // We hardcode tokenId_ for gas savings
        if (tokenId_ <= 3259) { return true; }
        return _ownerOf[tokenId_] != address(0x0);
    }

    // Public Write Functions 
    function approve(address to_, uint256 tokenId_) public virtual {
        address _owner = ownerOf(tokenId_);
        require(to_ != _owner, "ERC721IMigrator: approve() cannot approve owner!");
        require(msg.sender == _owner // sender is the owner of the token
            || isApprovedForAll[_owner][msg.sender], // or isApprovedForAll for the owner
            "ERC721IMigrator: approve() Caller is not owner of isApprovedForAll!");
        _approve(to_, tokenId_);
    }
    // SetApprovalForAll - the msg.sender is always the subject of approval
    function setApprovalForAll(address operator_, bool approved_) public virtual {
        _setApprovalForAll(msg.sender, operator_, approved_);
    }

    // Transfers
    function transferFrom(address from_, address to_, uint256 tokenId_) public virtual {
        require(_isApprovedOrOwner(msg.sender, tokenId_), 
            "ERC721IMigrator: transferFrom() _isApprovedOrOwner = false!");
        _transfer(from_, to_, tokenId_);
    }
    function safeTransferFrom(address from_, address to_, uint256 tokenId_, bytes memory data_) public virtual {
        transferFrom(from_, to_, tokenId_);
        if (to_.code.length != 0) {
            (, bytes memory _returned) = to_.staticcall(abi.encodeWithSelector(0x150b7a02, msg.sender, from_, tokenId_, data_));
            bytes4 _selector = abi.decode(_returned, (bytes4));
            require(_selector == 0x150b7a02, "ERC721IMigrator: safeTransferFrom() to_ not ERC721Receivable!");
        }
    }
    function safeTransferFrom(address from_, address to_, uint256 tokenId_) public virtual {
        safeTransferFrom(from_, to_, tokenId_, "");
    }

    // Native Multi-Transfers by 0xInuarashi
    function multiTransferFrom(address from_, address to_, uint256[] memory tokenIds_) public virtual {
        for (uint256 i = 0; i < tokenIds_.length; i++) {
            transferFrom(from_, to_, tokenIds_[i]);
        }
    }
    function multiSafeTransferFrom(address from_, address to_, uint256[] memory tokenIds_, bytes memory data_) public virtual {
        for (uint256 i = 0; i < tokenIds_.length; i++ ){
            safeTransferFrom(from_, to_, tokenIds_[i], data_);
        }
    }

    // OZ Standard Stuff
    function supportsInterface(bytes4 interfaceId_) public pure returns (bool) {
        return (interfaceId_ == 0x80ac58cd || interfaceId_ == 0x5b5e139f);
    }

    // High Gas Loop View Functions
    function walletOfOwner(address address_) public virtual view returns (uint256[] memory) {
        uint256 _balance = balanceOf(address_);
        uint256[] memory _tokens = new uint256[](_balance);
        uint256 _index;
        uint256 _loopThrough = totalSupply;
        for (uint256 i = 0; i < _loopThrough; i++) {
            // Add another loop through for each 0x0 until array is filled
            if (ownerOf(i) == address(0x0) && _tokens[_balance - 1] == 0) {
                _loopThrough++;
            }
            // Fill the array on each token found
            if (ownerOf(i) == address_) {
                // Record the ID in the index 
                _tokens[_index] = i;
                // Increment the index
                _index++;
            }
        }
        return _tokens;
    }

    // TokenURIs Functions Omitted //

}

interface IERC721 {
    function ownerOf(uint256 tokenId_) external view returns (address);
    function transferFrom(address from_, address to_, uint256 tokenId_) external;
}

interface iCS {
    struct Character {
        uint8  race_;
        uint8  renderType_;
        uint16 transponderId_;
        uint16 spaceCapsuleId_;
        uint8  augments_;
        uint16 basePoints_;
        uint16 totalEquipmentBonus_;
    }
    struct Stats {
        uint8 strength_; 
        uint8 agility_; 
        uint8 constitution_; 
        uint8 intelligence_; 
        uint8 spirit_; 
    }
    struct Equipment {
        uint8 weaponUpgrades_;
        uint8 chestUpgrades_;
        uint8 headUpgrades_;
        uint8 legsUpgrades_;
        uint8 vehicleUpgrades_;
        uint8 armsUpgrades_;
        uint8 artifactUpgrades_;
        uint8 ringUpgrades_;
    }

    // Create Character
    function createCharacter(uint tokenId_, Character memory Character_) external;
    // Characters
    function setName(uint256 tokenId_, string memory name_) external;
    function setRace(uint256 tokenId_, string memory race_) external;
    function setRenderType(uint256 tokenId_, uint8 renderType_) external;
    function setTransponderId(uint256 tokenId_, uint16 transponderId_) external;
    function setSpaceCapsuleId(uint256 tokenId_, uint16 spaceCapsuleId_) external;
    function setAugments(uint256 tokenId_, uint8 augments_) external;
    function setBasePoints(uint256 tokenId_, uint16 basePoints_) external;
    function setBaseEquipmentBonus(uint256 tokenId_, uint16 baseEquipmentBonus_) external;
    function setTotalEquipmentBonus(uint256 tokenId_, uint16 totalEquipmentBonus) external;
    // Stats
    function setStrength(uint256 tokenId_, uint8 strength_) external;
    function setAgility(uint256 tokenId_, uint8 agility_) external;
    function setConstitution(uint256 tokenId_, uint8 constitution_) external;
    function setIntelligence(uint256 tokenId_, uint8 intelligence_) external;
    function setSpirit(uint256 tokenId_, uint8 spirit_) external;
    // Equipment
    function setWeaponUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    function setChestUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    function setHeadUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    function setLegsUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    function setVehicleUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    function setArmsUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    function setArtifactUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    function setRingUpgrades(uint256 tokenId_, uint8 upgrade_) external;
    // Structs and Mappings
    function names(uint256 tokenId_) external view returns (string memory);
    function characters(uint256 tokenId_) external view returns (Character memory);
    function stats(uint256 tokenId_) external view returns (Stats memory);
    function equipments(uint256 tokenId_) external view returns (Equipment memory);
    function contractToRace(address contractAddress_) external view returns (uint8);
}

interface iCC {
    function queryCharacterYieldRate(uint8 augments_, uint16 basePoints_, uint16 totalEquipmentBonus_) external view returns (uint256);
    function getEquipmentBaseBonus(uint16 spaceCapsuleId_) external view returns (uint16); 
}

interface iMES {
    // View Functions
    function balanceOf(address address_) external view returns (uint256);
    function pendingRewards(address address_) external view returns (uint256); 
    // Administration
    function setYieldRate(address address_, uint256 yieldRate_) external;
    function addYieldRate(address address_, uint256 yieldRateAdd_) external;
    function subYieldRate(address address_, uint256 yieldRateSub_) external;
    // Credits System
    function deductCredits(address address_, uint256 amount_) external;
    function addCredits(address address_, uint256 amount_) external;
    // Claiming
    function updateReward(address address_) external;
    function burn(address from, uint256 amount_) external;
}

interface iMetadata {
    function renderMetadata(uint256 tokenId_) external view returns (string memory);
}

contract MTMCharacters is ERC721IMigrator {
    constructor() ERC721IMigrator("MTM Characters", "CHARACTERS") {}

    // Interfaces
    iCS public CS; iCC public CC; iMES public MES; iMetadata public Metadata;
    IERC721 public TP; IERC721 public SC;
    function setContracts(address metadata_, address cc_, address cs_, address mes_, address tp_, address sc_) external onlyOwner {
        CS = iCS(cs_); CC = iCC(cc_); MES = iMES(mes_); Metadata = iMetadata(metadata_);
        TP = IERC721(tp_); SC = IERC721(sc_);
    }

    // Mappings
    mapping(address => mapping(uint256 => bool)) public contractAddressToTokenUploaded;

    // Internal Write Functions
    function __yieldMintHook(address to_, uint256 tokenId_) internal {
        // First, we update the reward. 
        MES.updateReward(to_);

        // Then, we query the token yield rate.
        iCS.Character memory _Character = CS.characters(tokenId_);
        uint256 _tokenYieldRate = CC.queryCharacterYieldRate(_Character.augments_, _Character.basePoints_, _Character.totalEquipmentBonus_);

        // Lastly, we adjust the yield rate of the address.
        MES.addYieldRate(to_, _tokenYieldRate);
    }
    function __yieldTransferHook(address from_, address to_, uint256 tokenId_) internal {
        // First, we update the reward. 
        MES.updateReward(from_); MES.updateReward(to_);

        // Then, we query the token yield rate.
        iCS.Character memory _Character = CS.characters(tokenId_);
        uint256 _tokenYieldRate = CC.queryCharacterYieldRate(_Character.augments_, _Character.basePoints_, _Character.totalEquipmentBonus_);

        // Lastly, we adjust the yield rate of the addresses.
        MES.subYieldRate(from_, _tokenYieldRate); MES.addYieldRate(to_, _tokenYieldRate);
    }

    // Public Write Functions
    mapping(uint8 => bool) public renderTypeAllowed;
    function setRenderTypeAllowed(uint8 renderType_, bool bool_) external onlyOwner {
        renderTypeAllowed[renderType_] = bool_;
    }

    function beamCharacter(uint256 transponderId_, uint256 spaceCapsuleId_, uint8 renderType_) public {
        require(msg.sender == TP.ownerOf(transponderId_) && msg.sender == SC.ownerOf(spaceCapsuleId_), "Unowned pair!");
        require(renderTypeAllowed[renderType_], "This render type is not allowed!");

        // Burn the Transponder and Space Capsule.
        TP.transferFrom(msg.sender, address(this), transponderId_);
        SC.transferFrom(msg.sender, address(this), spaceCapsuleId_);

        uint8 _race = uint8( (uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty, transponderId_, spaceCapsuleId_))) % 10) + 1 ); // RNG (1-10) 
        uint16 _equipmentBonus = CC.getEquipmentBaseBonus((uint16(spaceCapsuleId_)));

        iCS.Character memory _Character = iCS.Character(
            _race,
            renderType_,
            uint16(transponderId_),
            uint16(spaceCapsuleId_),
            0,
            0,
            _equipmentBonus
        );

        CS.createCharacter(totalSupply, _Character);
        
        __yieldMintHook(msg.sender, totalSupply);
        _mint(msg.sender, totalSupply);
    }
    function uploadCharacter(uint256 transponderId_, uint256 spaceCapsuleId_, uint8 renderType_, address contractAddress_, uint256 uploadId_) public {
        require(msg.sender == TP.ownerOf(transponderId_) && msg.sender == SC.ownerOf(spaceCapsuleId_), "Unowned pair!");
        require(msg.sender == IERC721(contractAddress_).ownerOf(uploadId_), "You don't own this character!");
        require(!contractAddressToTokenUploaded[contractAddress_][uploadId_], "This character has already been uploaded!");
        require(renderTypeAllowed[renderType_], "This render type is not allowed!");

        // Burn the Transponder and Space Capsule. Then, set the character as uploaded.
        TP.transferFrom(msg.sender, address(this), transponderId_);
        SC.transferFrom(msg.sender, address(this), spaceCapsuleId_);
        contractAddressToTokenUploaded[contractAddress_][uploadId_] = true;

        uint8 _race = CS.contractToRace(contractAddress_);
        uint16 _equipmentBonus = CC.getEquipmentBaseBonus((uint16(spaceCapsuleId_)));
        
        iCS.Character memory _Character = iCS.Character(
            _race,
            renderType_,
            uint16(transponderId_),
            uint16(spaceCapsuleId_),
            0,
            0,
            _equipmentBonus
        );

        CS.createCharacter(totalSupply, _Character); 

        __yieldMintHook(msg.sender, totalSupply);
        _mint(msg.sender, totalSupply); 
    }

    // Public Write Multi-Functions
    function multiBeamCharacter(uint256[] memory transponderIds_, uint256[] memory spaceCapsuleIds_, uint8[] memory renderTypes_) public {
        require(transponderIds_.length == spaceCapsuleIds_.length, "Missing pairs!");
        require(transponderIds_.length == renderTypes_.length, "Missing render type!");
        for (uint256 i = 0; i < transponderIds_.length; i++) {
            beamCharacter(transponderIds_[i], spaceCapsuleIds_[i], renderTypes_[i]);
        }
    }
    function multiUploadCharacter(uint256[] memory transponderIds_, uint256[] memory spaceCapsuleIds_, uint8[] memory renderTypes_, address contractAddress_, uint256[] memory uploadIds_) public {
        require(transponderIds_.length == spaceCapsuleIds_.length, "Missing pairs!");
        require(transponderIds_.length == renderTypes_.length, "Missing render type!");
        require(transponderIds_.length == uploadIds_.length, "Upload IDs mismatched length!");
        for (uint256 i = 0; i < transponderIds_.length; i++) {
            uploadCharacter(transponderIds_[i], spaceCapsuleIds_[i], renderTypes_[i], contractAddress_, uploadIds_[i]);
        }
    }

    // Transfer Hooks
    function transferFrom(address from_, address to_, uint256 tokenId_) public override {
        __yieldTransferHook(from_, to_, tokenId_);
        ERC721IMigrator.transferFrom(from_, to_, tokenId_);
    }
    function safeTransferFrom(address from_, address to_, uint256 tokenId_, bytes memory bytes_) public override {
        __yieldTransferHook(from_, to_, tokenId_);
        ERC721IMigrator.safeTransferFrom(from_, to_, tokenId_, bytes_);
    }

    // Public View Functions
    function tokenURI(uint256 tokenId_) public view returns (string memory) {
        require(_exists(tokenId_), "Character does not exist!");
        return Metadata.renderMetadata(tokenId_);
    }
}