contract ArgoBadAssistant is ERC721, ERC721Enumerable, Ownable, ReentrancyGuard, Pausable {
    using SafeMath for uint256;
    using Strings for string;

    string private _baseUriString;
    uint public salesStatus = 0;
    uint256 public maxPersons = 10000;
    uint256 public lastPersonID;
    uint256 public reservedPersons = 200;
    uint256 public personPrice = 0.04 ether;
    mapping(uint => bool) public personType;
    mapping(uint => bool) public tokenLockedStates;
    mapping(address => bool) public contractGameControllers;

    bool public isMergeOnTransfer = true;

    constructor(string memory baseUriString) ERC721("Argo Bad Assistant", "ArgoBadAssistant") {
        setBaseUri(baseUriString);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId)
    internal
    override(ERC721, ERC721Enumerable)
    {
        super._beforeTokenTransfer(from, to, tokenId);
        require(!paused(), "ERC721Pausable: token transfer while paused");
        require(!tokenLockedStates[tokenId]);
    }

    function setBaseUri(string memory baseUriString) public onlyOwner {
        _baseUriString = baseUriString;
    }

    function _baseURI() internal view override returns (string memory) {
        return _baseUriString;
    }

    modifier onlyController() {
        require(contractGameControllers[_msgSender()] == true, "Not authorized");
        _;
    }

    function addGameController(address controllerAddress) public onlyOwner {
        contractGameControllers[controllerAddress] = true;
    }

    function removeGameController(address controllerAddress) public onlyOwner {
        delete contractGameControllers[controllerAddress];
    }

    function tokenURI(uint256 tokenId) public view override(ERC721) returns (string memory) {
        string memory _tokenURI = super.tokenURI(tokenId);
        return bytes(_tokenURI).length > 0 ? string(abi.encodePacked(_tokenURI, ".json")) : "";
    }

    function tokensOfOwner(address _owner) external view returns (uint256[] memory) {
        uint256 tokenCount = balanceOf(_owner);
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    function _setPersonType(uint personId, bool typeBoolean) private {
        personType[personId] = typeBoolean;
    }

    function setPersonType(uint personId, bool typeBoolean) public onlyController {
        _setPersonType(personId, typeBoolean);
    }

    function _personMint(address to, uint count) private {
        uint256 _lastPersonID = lastPersonID;
        bool previousLastPersonType = personType[lastPersonID];
        uint toTokenAmount = balanceOf(to);
        if (count == 1) {
            if (toTokenAmount == 1) {
                _lastPersonID++;
                _safeMint(to, _lastPersonID);
                _setPersonType(_lastPersonID, !personType[tokenOfOwnerByIndex(to, 0)]);
            } else {
                _lastPersonID++;
                previousLastPersonType = !previousLastPersonType;
                if (toTokenAmount == 2) {
                    uint token1 = tokenOfOwnerByIndex(to, 0);
                    uint token2 = tokenOfOwnerByIndex(to, 1);
                    if (personType[token1] == previousLastPersonType) {
                        _burnToken(token1);
                    } else {
                        _burnToken(token2);
                    }
                }
                _safeMint(to, _lastPersonID);
                _setPersonType(_lastPersonID, previousLastPersonType);
            }
        } else {
            if (toTokenAmount == 1) {
                _burnToken(tokenOfOwnerByIndex(to, 0));
            }
            if (toTokenAmount == 2) {
                uint token1 = tokenOfOwnerByIndex(to, 0);
                uint token2 = tokenOfOwnerByIndex(to, 1);
                _burnToken(token1);
                _burnToken(token2);
            }
            _lastPersonID++;
            previousLastPersonType = !previousLastPersonType;
            _safeMint(to, _lastPersonID);
            _setPersonType(_lastPersonID, previousLastPersonType);

            _lastPersonID++;
            previousLastPersonType = !previousLastPersonType;
            _safeMint(to, _lastPersonID);
            _setPersonType(_lastPersonID, previousLastPersonType);
        }
        lastPersonID = _lastPersonID;
    }

    function personMint(uint256 count) external payable nonReentrant {
        uint cards = count == 1 ? 1 : 2;
        require(
            (salesStatus == 2 || salesStatus == 1)
            && count > 0
            && totalSupply().add(cards) <= maxPersons.sub(reservedPersons)
            && msg.value >= personPrice.mul(count)
        );
        _personMint(msg.sender, cards);
    }

    function reservedPersonMint(address to, uint256 count) public onlyOwner {
        if (to == address(0)) {
            to = msg.sender;
        }
        uint cards = count == 1 ? 1 : 2;
        require(count > 0 && cards <= reservedPersons);
        _personMint(to, cards);
        reservedPersons -= cards;
    }

    function changeSalesStatus(uint status) public onlyOwner {
        salesStatus = status;
    }

    function updatePrice(uint256 amount) public onlyOwner {
        require(amount >= 0);
        personPrice = amount;
    }

    function withdrawTo(address to, uint256 amount) public payable onlyOwner {
        require(amount > 0);
        if (to == address(0)) {
            to = msg.sender;
        }
        require(payable(to).send(amount));
    }

    function withdraw() public payable onlyOwner {
        require(payable(owner()).send(address(this).balance));
    }

    // burn token
    function _burnToken(uint256 tokenId) private {
        address from = ERC721.ownerOf(tokenId);

        // Clear approvals
        _approve(address(0), tokenId);

        _removeTokenFromOwnerEnumeration(from, tokenId);
        _removeTokenFromAllTokensEnumeration(tokenId);

        _balances[from] -= 1;
        delete _owners[tokenId];

        emit Transfer(from, address(0), tokenId);
    }

    function burnToken(uint256 tokenId) public onlyController {
        _burnToken(tokenId);
    }

    function _managedMint(address to, uint256 tokenId) private {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _addTokenToAllTokensEnumeration(tokenId);
        _addTokenToOwnerEnumeration(to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    // create token with params - type, owner
    function createToken(address to, bool characterType) public onlyController {
        _createPerson(to, characterType);
    }

    function _createPerson(address to, bool characterType) private {
        lastPersonID++;
        _managedMint(to, lastPersonID);
        _setPersonType(lastPersonID, characterType);
    }

    // change token owner
    function reassignToken(address from, address to, uint256 tokenId) public onlyController {
        require(to != address(0), "ERC721: transfer to the zero address");

        // Clear approvals from the previous owner
        _approve(address(0), tokenId);

        _removeTokenFromOwnerEnumeration(from, tokenId);
        _addTokenToOwnerEnumeration(to, tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    // lock token for interaction
    function lockToken(uint256 tokenId) public onlyController {
        tokenLockedStates[tokenId] = true;
    }

    // unlock token for interaction
    function unlockToken(uint256 tokenId) public onlyController {
        tokenLockedStates[tokenId] = false;
    }

    // Toggle contract direct interactions
    function togglePauseContract() public onlyController {
        paused() ? _unpause() : _pause();
    }

    function _transfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override {
        super._transfer(from, to, tokenId);
        _normalize(to, personType[tokenId]);
    }

    function _normalize(address to, bool tokenType) private {
        if (isMergeOnTransfer) {
            uint toTokenAmount = balanceOf(to);
            uint256 index;
            uint countType = 0;
            for (index = 0; index < toTokenAmount; index++) {
                uint token = tokenOfOwnerByIndex(to, index);
                if (personType[token] == tokenType) {
                    countType++;
                }
            }
            uint256[] memory toBurnTokens = new uint256[](countType);
            uint burnCounter = 0;
            for (index = 0; index < toTokenAmount; index++) {
                uint token = tokenOfOwnerByIndex(to, index);
                if (personType[token] == tokenType) {
                    toBurnTokens[burnCounter] = token;
                    burnCounter++;
                }
            }
            if (toBurnTokens.length > 1) {
                for (index = 0; index < toBurnTokens.length; index++) {
                    _burnToken(toBurnTokens[index]);
                }
                _createPerson(to, tokenType);
            }
        }
    }

    function normalizeByType(address to, bool tokenType) public onlyController {
        _normalize(to, tokenType);
    }

    function toggleIsMergeOnTransfer() public onlyOwner {
        isMergeOnTransfer = !isMergeOnTransfer;
    }
}
function _burnToken(uint256 tokenId) private {
    address from = ERC721.ownerOf(tokenId);

    // Clear approvals
    _approve(address(0), tokenId);

    _removeTokenFromOwnerEnumeration(from, tokenId);
    _removeTokenFromAllTokensEnumeration(tokenId);

    _balances[from] -= 1;
    delete _owners[tokenId];

    emit Transfer(from, address(0), tokenId);
}

// burn token