contract HumansOfTheMetaverseRealEstate is ERC721Enum, Ownable, Pausable {
    using EnumerableSet for EnumerableSet.UintSet;
    using EnumerableSet for EnumerableSet.AddressSet;
    using Strings for uint256;

    uint8 bonusPercentage = 20;

    // business logic constants
    uint16 private constant LAND = 1;
    uint256 private constant ETHER = 1e18;

    string private baseURI;
    address public tokenAddress;

    struct Coordinate {
        int32 layer;
        int256 x;
        int256 y;
    }

    struct LayerConfiguration {
        bool lock;
        int32 layer;
        uint256 length;
        mapping(uint16 => uint32) typesPricing; // real estate prices will be dependent on the layer they are in
        mapping(uint256 => uint256) rangePricing;
    }

    EnumerableSet.AddressSet collections;
    EnumerableSet.AddressSet allowedRealEstateModifiers;

    mapping(int32 => LayerConfiguration) layerConfigurationMap;

    mapping(bytes32 => uint8) buildingEnrolmentSlotsMapping; // layer + buildingType => Enrolment

    mapping(bytes32 => bool) realEstateOccupancyMapping; // layer + x + y => bool

    mapping(bytes32 => bool) humanEnrolmentMapping; // address + tokenId => layer + x + y

    mapping(uint256 => EnumerableSet.UintSet) tokenEnrolmentEmployeesMapping;
    mapping(uint256 => uint64) tokensTypeMapping;
    mapping(uint256 => Coordinate) tokensCoordinates;

    // events
    event RealEstateMinted(uint256[] tokenIds, Coordinate[] coordinates, address caller);
    event RealEstateChanges(uint256[] tokenIds, uint16[] realEstateTypes, address caller);
    event RealEstateEnrolment(uint256[] tokenIds, uint256 office, uint256 timestamp, address caller, address collection);
    event RealEstateEnrolmentRetrieval(uint256[] tokenIds, uint256 office, uint256 timestamp, address caller, address collection);


    constructor(
        string memory _name,
        string memory _symbol,
        string memory _initBaseURI,
        address[] memory _collectionAddresses,
        address _tokenAddress
    ) ERC721P(_name, _symbol) {
        _pause();
        setBaseURI(_initBaseURI);

        for (uint8 i = 0; i < _collectionAddresses.length; ++i) {
            collections.add(_collectionAddresses[i]);
        }

        tokenAddress = _tokenAddress;
    }

    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        baseURI = _newBaseURI;
    }

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    function setTokenAddress(address _address) external onlyOwner {
        tokenAddress = _address;
    }

    function addAllowedModifiers(address[] calldata _modifierAddresses) external onlyOwner {
        for (uint8 i = 0; i < _modifierAddresses.length; ++i) {
            allowedRealEstateModifiers.add(_modifierAddresses[i]);
        }
    }

    function removeAllowedModifiers(address[] calldata _modifierAddresses) external onlyOwner {
        for (uint8 i = 0; i < _modifierAddresses.length; ++i) {
            allowedRealEstateModifiers.remove(_modifierAddresses[i]);
        }
    }

    function addCollection(address _collection) external onlyOwner {
        collections.add(_collection);
    }

    function removeCollection(address _collection) external onlyOwner {
        require(collections.contains(_collection), "Specified address not found");
        collections.remove(_collection);
    }

    function setOfficeBoost(uint8 _boost) external onlyOwner {
        bonusPercentage = _boost;
    }

    function withdrawTokens() external onlyOwner {
        IERC20(tokenAddress).transfer(msg.sender, IERC20(tokenAddress).balanceOf(address(this)));
    }

    function setLayerConfiguration(
        int32 _layer,
        uint256 _length,
        uint16[] calldata _realEstateTypes,
        uint32[] calldata _realEstatePrices,
        uint8[] calldata _slots,
        uint256[] calldata _landPrices
    ) external onlyOwner {
        require(
            _realEstateTypes.length == _realEstatePrices.length
            && _realEstatePrices.length == _slots.length
            && _landPrices.length == _length / 2,
            "Incorrect input"
        );

        LayerConfiguration storage layerConfiguration = layerConfigurationMap[_layer];
        layerConfiguration.layer = _layer;
        layerConfiguration.length = _length;

        for (uint8 i = 0; i < _realEstateTypes.length; ++i) {
            layerConfiguration.typesPricing[_realEstateTypes[i]] = _realEstatePrices[i];
            buildingEnrolmentSlotsMapping[
            keccak256(abi.encode(_layer, _realEstateTypes[i]))
            ] = _slots[i];
        }

        for(uint256 i = 0; i < _length / 2; ++i) {
            layerConfiguration.rangePricing[i * 2] = _landPrices[i];
        }
    }

    function setLayerLock(int32 _layer, bool _lock) external onlyOwner {
        LayerConfiguration storage layerConfiguration = _getLayerConfiguration(_layer);
        layerConfiguration.lock = _lock;
    }

    function setLayerLength(int32 _layer, uint256 _length, uint256[] calldata _additionalPrices) external onlyOwner {
        LayerConfiguration storage layerConfiguration = _getLayerConfiguration(_layer);

        require(_additionalPrices.length == (_length / 2 - layerConfiguration.length / 2), "Incorrect input");

        for(uint256 i = layerConfiguration.length / 2; i < _length / 2; ++i) {
            layerConfiguration.rangePricing[i * 2] = _additionalPrices[i - layerConfiguration.length / 2];
        }

        layerConfiguration.length = _length;

    }

    function setLayerLandPricing(int32 _layer, uint256[] calldata prices) external onlyOwner {
        LayerConfiguration storage layerConfiguration = _getLayerConfiguration(_layer);

        require(prices.length == layerConfiguration.length / 2, "Incorrect input");

        for (uint256 i = 0; i < layerConfiguration.length / 2; ++i) {
            layerConfiguration.rangePricing[i * 2] = prices[i];
        }

    }

    function setLayerTypesPricing(
        int32 _layer,
        uint16[] calldata realEstateTypes,
        uint32[] calldata realEstatePrices
    ) external onlyOwner {
        require(realEstateTypes.length == realEstatePrices.length, "Incorrect input");
        LayerConfiguration storage layerConfiguration = _getLayerConfiguration(_layer);

        for (uint16 i = 0; i < realEstateTypes.length; ++i) {
            layerConfiguration.typesPricing[realEstateTypes[i]] = realEstatePrices[i];
        }
    }

    function removeLayerTypesPricing(int32 _layer, uint16[] calldata realEstateTypes) external onlyOwner {
        LayerConfiguration storage layerConfiguration = _getLayerConfiguration(_layer);
        for (uint256 i = 0; i < realEstateTypes.length; ++i) {
            delete layerConfiguration.typesPricing[realEstateTypes[i]];
            delete buildingEnrolmentSlotsMapping[keccak256(abi.encode(_layer, realEstateTypes[i]))];
        }
    }

    function setLayerRealEstateTypeEnrolmentConfig(int32 _layer, uint16[] calldata _realEstateTypes, uint8[] calldata _slots) external onlyOwner {
        require(_realEstateTypes.length == _slots.length, "Incorrect input");
        for (uint64 i = 0; i < _realEstateTypes.length; ++i) {
            buildingEnrolmentSlotsMapping[keccak256(abi.encode(_layer, _realEstateTypes[i]))] = _slots[i];
        }
    }

    function _baseURI() internal view virtual returns (string memory) {
        return baseURI;
    }

    function _getLayerConfiguration(int32 _layer) internal view returns (LayerConfiguration storage) {
        LayerConfiguration storage layerConfiguration = layerConfigurationMap[_layer];
        require(layerConfiguration.length != uint256(0), "There is no configuration for provided layer");

        return layerConfiguration;
    }

    function _abs(int256 x) internal pure returns(uint256) {
        return x >= 0 ? uint256(x) : uint256(-x);
    }

    function buyLand(Coordinate[] memory _coordinates) external whenNotPaused payable {
        uint256 price = calculateLandPrice(_coordinates);

        uint256[] memory tokenIds = occupyLand(_coordinates);

        IERC20(tokenAddress).transferFrom(msg.sender, address(this), price *  ETHER);

        emit RealEstateMinted(tokenIds, _coordinates, msg.sender);

        delete price;
        delete tokenIds;
    }

    function changeRealEstate(uint256[] calldata _tokenIds, uint16[] calldata _realEstateNewTypes) external whenNotPaused payable {
        require(_tokenIds.length == _realEstateNewTypes.length, "Incorrect input dat");

        validateTokensToBeChanged(_tokenIds, _realEstateNewTypes);

        uint256 price = calculateRealEstatePrice(_tokenIds, _realEstateNewTypes);

        for (uint64 i = 0; i < _tokenIds.length; ++i) {
            tokensTypeMapping[_tokenIds[i]] = _realEstateNewTypes[i];
        }

        IERC20(tokenAddress).transferFrom(msg.sender, address(this), price * ETHER);

        emit RealEstateChanges(_tokenIds, _realEstateNewTypes, msg.sender);

        delete price;
    }

    function externalChangeRealEstate(uint256[] calldata _tokenIds, uint16[] calldata _realEstateNewTypes, address owner, uint256 amount) external whenNotPaused {
        require(allowedRealEstateModifiers.contains(msg.sender), "Not an allowed real estate changer");
        require(_tokenIds.length == _realEstateNewTypes.length, "Incorrect input data");

        validateTokensToBeChanged(_tokenIds, _realEstateNewTypes);

        for (uint64 i = 0; i < _tokenIds.length; ++i) {
            tokensTypeMapping[_tokenIds[i]] = _realEstateNewTypes[i];
        }

        IERC20(tokenAddress).transferFrom(owner, address(this),  amount * ETHER);

        emit RealEstateChanges(_tokenIds, _realEstateNewTypes, owner);
    }

    function validateTokensToBeChanged(uint256[] calldata _tokenIds, uint16[] calldata _realEstateNewTypes) internal view {

        for (uint64 i = 0; i < _tokenIds.length; ++i) {
            if (tokensTypeMapping[_tokenIds[i]] == _realEstateNewTypes[i]) {
                revert("Operation not allowed");
            }

            require(msg.sender == ownerOf(_tokenIds[i]), "Not the owner of the real estate piece");
        }
    }

    function calculateLandPrice(Coordinate[] memory _coordinates) public view returns(uint256) {
        validateCoordinates(_coordinates);
        uint256 price = 0;

        for(uint128 i = 0; i < _coordinates.length; ++i) {
            Coordinate memory coordinate = _coordinates[i];
            LayerConfiguration storage layerConfiguration = _getLayerConfiguration(coordinate.layer);

            uint256 landPriceIndex = uint256(_abs(coordinate.x) + _abs(coordinate.y));

            landPriceIndex = landPriceIndex % 2 == 1 ? landPriceIndex - 1 : landPriceIndex;

            price += layerConfiguration.rangePricing[landPriceIndex];
        }

        return price;
    }

    function calculateRealEstatePrice(uint256[] calldata _tokenIds, uint16[] calldata _realEstateTypes) public view returns(uint256) {
        uint256 price = 0;

        for(uint128 i = 0; i < _tokenIds.length; ++i) {

            LayerConfiguration storage layerConfiguration = _getLayerConfiguration(tokensCoordinates[_tokenIds[i]].layer);

            uint256 realEstatePrice = layerConfiguration.typesPricing[_realEstateTypes[i]];

            require(realEstatePrice != uint32(0), "Unsupported building type");

            price += realEstatePrice;

        }

        return price;
    }

    function validateCoordinates(Coordinate[] memory _coordinates) public view {
        for (uint256 i = 0; i < _coordinates.length; ++i) {
            Coordinate memory coordinate = _coordinates[i];
            LayerConfiguration storage layerConfiguration = _getLayerConfiguration(coordinate.layer);

            require(
                _abs(coordinate.x) < layerConfiguration.length / 2
                && _abs(coordinate.y) < layerConfiguration.length / 2
                && !realEstateOccupancyMapping[keccak256(abi.encode(coordinate.x, coordinate.y, coordinate.layer))],
                "Coordinates invalid"
            );
        }
    }

    function enrollInOffice(uint256[] calldata _workers, uint256 _office, address _collection) external whenNotPaused {

        require(collections.contains(_collection), "Not amongst enrollable collections");
        require(ownerOf(_office) == msg.sender, "Not the owner of the office");

        if (
            buildingEnrolmentSlotsMapping[keccak256(abi.encode(tokensCoordinates[_office].layer, tokensTypeMapping[_office]))]
            - tokenEnrolmentEmployeesMapping[_office].length()
            < _workers.length
        ) {
            revert("The office does not have enough slots available");
        }

        for (uint8 i = 0; i < _workers.length; ++i) {
            require(IERC721(_collection).ownerOf(_workers[i]) == msg.sender, "Not the owner of the humans");
            require(!humanEnrolmentMapping[keccak256(abi.encode(_collection, _workers[i]))], "Human already enrolled");
            tokenEnrolmentEmployeesMapping[_office].add(_workers[i]);
            humanEnrolmentMapping[keccak256(abi.encode(_collection, _workers[i]))] = true;
        }

        uint256 value = block.timestamp;

        notifyYielder(value, _collection, _workers);

        emit RealEstateEnrolment(_workers, _office, value, msg.sender, _collection);

        delete value;

    }

    function retrieveHuman(uint256[] calldata _workers, uint256 _office, address _collection) external whenNotPaused {
        require(collections.contains(_collection));
        require(ownerOf(_office) == msg.sender);

        for (uint8 i = 0; i < _workers.length; ++i) {
            require(IERC721(_collection).ownerOf(_workers[i]) == msg.sender);
            require(humanEnrolmentMapping[keccak256(abi.encode(_collection, _workers[i]))], "Human not enrolled");
            require(tokenEnrolmentEmployeesMapping[_office].contains(_workers[i]), "Human not enrolled in specified office");
            tokenEnrolmentEmployeesMapping[_office].remove(_workers[i]);
            humanEnrolmentMapping[keccak256(abi.encode(_collection, _workers[i]))] = false;
        }

        notifyYielder(0, _collection, _workers);

        emit RealEstateEnrolmentRetrieval(_workers, _office, 0, msg.sender, _collection);
    }

    function notifyYielder(uint256 value, address _collection, uint256[] calldata tokens) internal {
        uint256[] memory startDates = new uint256[](tokens.length);
        for (uint8 i = 0; i < startDates.length; ++i) {
            startDates[i] = value;
        }

        IBoostWatcher(tokenAddress).watchBooster(_collection, tokens, startDates);

        delete startDates;
    }

    function lockCoordinates(Coordinate[] memory _coordinates) external onlyOwner {
        validateCoordinates(_coordinates);

        for (uint64 i = 0; i < _coordinates.length; ++i) {
            if (!realEstateOccupancyMapping[keccak256(abi.encode(_coordinates[i].x, _coordinates[i].y, _coordinates[i].layer))]) {
                realEstateOccupancyMapping[keccak256(abi.encode(_coordinates[i].x, _coordinates[i].y, _coordinates[i].layer))] = true;
            }
        }

    }

    function unlockCoordinates(Coordinate[] memory _coordinates) external onlyOwner {
        for (uint64 i = 0; i < _coordinates.length; ++i) {
            if (realEstateOccupancyMapping[keccak256(abi.encode(_coordinates[i].x, _coordinates[i].y, _coordinates[i].layer))]) {
                realEstateOccupancyMapping[keccak256(abi.encode(_coordinates[i].x, _coordinates[i].y, _coordinates[i].layer))] = false;
            }
        }

    }

    function reserveCoordinates(Coordinate[] memory _coordinates) external onlyOwner {
        validateCoordinates(_coordinates);

        uint256[] memory tokenIds = occupyLand(_coordinates);

        for (uint64 i = 0; i < _coordinates.length; ++i) {
            if (!realEstateOccupancyMapping[keccak256(abi.encode(_coordinates[i].x, _coordinates[i].y, _coordinates[i].layer))]) {
                realEstateOccupancyMapping[keccak256(abi.encode(_coordinates[i].x, _coordinates[i].y, _coordinates[i].layer))] = true;
            }
        }

        emit RealEstateMinted(tokenIds, _coordinates, msg.sender);
    }

    function occupyLand(Coordinate[] memory _coordinates) internal returns(uint256[] memory ){
        uint256[] memory tokenIds = new uint256[](_coordinates.length);
        uint256 totalSupply = totalSupply();

        for (uint256 i = 0; i < _coordinates.length; ++i) {
            tokenIds[i] = totalSupply + i;
            tokensTypeMapping[totalSupply + i] = LAND;
            tokensCoordinates[totalSupply + i] = _coordinates[i];
            realEstateOccupancyMapping[keccak256(abi.encode(_coordinates[i].x, _coordinates[i].y, _coordinates[i].layer))] = true;
            _safeMint(msg.sender, totalSupply + i);
        }

        return tokenIds;
    }

    // backend

    function getTokensType(uint256[] calldata _tokenIds) public view returns(uint256[] memory) {
        uint256[] memory types = new uint256[](_tokenIds.length);
        for (uint64 i = 0; i < _tokenIds.length; ++i) {
            types[i] = tokensTypeMapping[_tokenIds[i]];
        }

        return types;
    }

    function getTokensCoordinates(uint256[] calldata _tokenIds) public view returns(Coordinate[] memory) {
        Coordinate[] memory coordinates = new Coordinate[](_tokenIds.length);
        for (uint64 i = 0; i < _tokenIds.length; ++i) {
            coordinates[i] = tokensCoordinates[_tokenIds[i]];
        }

        return coordinates;
    }

    function getTokensEnrolments(uint256[] calldata _tokenIds) public view returns(uint256[][] memory) {
        uint256[][] memory enrolments = new uint256[][](_tokenIds.length);
        for (uint64 i = 0; i < _tokenIds.length; ++i) {
            enrolments[_tokenIds[i]] = new uint256[](tokenEnrolmentEmployeesMapping[_tokenIds[i]].length());

            for (uint8 j = 0; j < tokenEnrolmentEmployeesMapping[_tokenIds[i]].length(); ++i) {
                enrolments[_tokenIds[i]][j] = tokenEnrolmentEmployeesMapping[_tokenIds[i]].at(j);
            }
        }

        return enrolments;
    }

    function getTokensOwners(uint256[] calldata _tokenIds) external view returns(address[] memory) {
        address[] memory addresses = new address[](_tokenIds.length);

        for(uint64 i = 0; i < _tokenIds.length; ++i) {
            addresses[i] = ownerOf(_tokenIds[i]);
        }

        return addresses;
    }

    // overrides

    function tokenURI(uint256 _tokenId) external view virtual override returns (string memory) {
        require(_exists(_tokenId), "ERC721Metadata: Nonexistent token");
        string memory currentBaseURI = _baseURI();
        return bytes(currentBaseURI).length > 0	? string(abi.encodePacked(currentBaseURI, _tokenId.toString(), ".json")) : "";
    }

    // boosting

    function computeAmount(uint256 amount) external view returns(uint256) {
        return amount + amount * bonusPercentage / 100;
    }

}
function tokenURI(uint256 _tokenId) external view virtual override returns (string memory) {
    require(_exists(_tokenId), "ERC721Metadata: Nonexistent token");
    string memory currentBaseURI = _baseURI();
    return bytes(currentBaseURI).length > 0	? string(abi.encodePacked(currentBaseURI, _tokenId.toString(), ".json")) : "";
}

// overrides