contract Landemic is ERC721Full("Landemic","LAND") {

    uint256 public _basePrice = 810000000000000; // $0.25 ETH/USD as of May 2019
    uint8 public _bountyDivisor = 20; // 5% to each player
    uint16 public _defaultMultiple = 100; // using 1 decimal point divisibility, max 1000
    address public _owner = msg.sender;
    string public _baseURL = "https://landemic.io/";

    struct Price {
        uint240 lastPrice;
        uint16 multiple;
    }
    mapping(uint256 => Price) public _prices;

    // When recepient cannot receive the bounty,
    // it is stored here and can be pulled manually
    mapping(address => uint256) public failedPayouts;
    uint256 public failedPayoutsSum;

    /* Admin */

    modifier onlyContractOwner() {
        require(msg.sender == _owner);
        _;
    }

    function _lastPrice(uint256 tokenId) public view returns (uint256) {
        return uint256(_prices[tokenId].lastPrice);
    }

    function _multiple(uint256 tokenId) public view returns (uint16) {
        return _prices[tokenId].multiple;
    }

    function setBasePrice(uint256 basePrice) onlyContractOwner public {
        _basePrice = basePrice;
    }

    function setBountyDivisor(uint8 bountyDivisor) onlyContractOwner public {
        _bountyDivisor = bountyDivisor;
    }

    function setBaseURL(string baseURL) public onlyContractOwner {
        _baseURL = baseURL;
    }

    function setOwner(address owner) onlyContractOwner public {
        _owner = owner;
    }

    // Allows the owner to withdraw profits and failed payouts.
    // Owner should check failedPayoutsSum to not withdraw extra.
    function withdraw(uint256 amount) onlyContractOwner public {
        msg.sender.transfer(amount);
    }

    function withdrawProfit() onlyContractOwner public {
        msg.sender.transfer(address(this).balance.sub(failedPayoutsSum));
    }

    /* Temp Functions Until Server-Side Component */

    function getAllOwned() public view returns (uint256[], address[]) {

        uint totalOwned = totalSupply();

        uint256[] memory ownedUint256 = new uint256[](totalOwned);
        address[] memory ownersAddress = new address[](totalOwned);

        for (uint i = 0; i < totalOwned; i++) {
            ownedUint256[i] = tokenByIndex(i);
            ownersAddress[i] = ownerOf(ownedUint256[i]);
        }

        return (ownedUint256, ownersAddress);

    }

    function metadataForToken(uint256 tokenId) public view returns (uint256, address, uint16, uint256) {
        uint256 price = priceOf(tokenId);

        if (_exists(tokenId)) {
            return (_lastPrice(tokenId), ownerOf(tokenId), multipleOf(tokenId), price);
        }
        return (_basePrice, 0, 10, price);
    }

    function priceOf(uint256 tokenId) public view returns (uint256) {
        if (_exists(tokenId)) {
            return _lastPrice(tokenId).mul(uint256(multipleOf(tokenId))).div(10);
        }
        return _basePrice;
    }

    function multipleOf(uint256 tokenId) public view returns (uint16) {
        uint16 multiple = _multiple(tokenId);
        if (multiple > 0) {
            return multiple;
        }
        return _defaultMultiple;
    }

    /* Adjust metadata */

    modifier onlyTokenOwner(uint256 tokenId) {
        require(msg.sender == ownerOf(tokenId));
        _;
    }

    function setMultiple(uint256 tokenId, uint16 multiple) public onlyTokenOwner(tokenId) {
        require(multiple >= 1 && multiple <= 1000);
        _prices[tokenId].multiple = multiple;
    }

    /* Buy & Sell */

    // TODO:
    // function grabCodes(uint256[] tokens) public payable {
    //     for (uint i = 0; i < tokens.length; i++) {
    //         _mint(msg.sender, tokens[i]);
    //     }
    // }



    function _pushOrDelayBounty(address to, uint256 amount) internal {
        bool success = to.send(amount);
        if (!success) {
            failedPayouts[to] = failedPayouts[to].add(amount);
            failedPayoutsSum = failedPayoutsSum.add(amount);
        }
    }

    function grabCode(uint256 tokenId) public payable {
        // uint256 tokenId = uint256(stringToBytes32(code));

        uint256 price = priceOf(tokenId);  // _lastPrice * 0.1  <=  price  <=  _lastPrice * 10
        require(msg.value >= price);

        _prices[tokenId] = Price(uint240(msg.value), uint16(0));

        if (!_exists(tokenId)) {
            _mint(msg.sender, tokenId);
            return;
        }

        address owner = ownerOf(tokenId);
        require(owner != msg.sender);

        _burn(owner, tokenId);
        _mint(msg.sender, tokenId);

        uint256 bounty = msg.value.div(_bountyDivisor);  // 5%
        uint256 bountiesCount = 1; // Landemic.
        uint256[4] memory neighbors = neighborsOfToken(tokenId);
        for (uint i = 0; i < 4; i++) {
            uint256 neighbor = neighbors[i];
            if (!_exists(neighbor)) {
                continue;
            }
            _pushOrDelayBounty(ownerOf(neighbor), bounty);
            bountiesCount++;
        }

        _pushOrDelayBounty(owner, msg.value.sub(bounty.mul(bountiesCount)));  // 75%
    }

    function pullBounty(address to) public {
        uint256 bounty = failedPayouts[msg.sender];
        if (bounty == 0) {
            return;
        }
        failedPayouts[msg.sender] = 0;
        failedPayoutsSum = failedPayoutsSum.sub(bounty);
        to.transfer(bounty);
    }


    /* ERC721Metadata overrides */

    /**
    * @dev Returns an URI for a given token ID
    * @dev Throws if the token ID does not exist. May return an empty string.
    * @param _tokenId uint256 ID of the token to query
    */
    function tokenURI(uint256 _tokenId) external view returns (string) {
        require(_exists(_tokenId));
        return strConcat(strConcat(_baseURL, uint256ToString(_tokenId)),".json");
    }

    /* Utilities */

    // https://ethereum.stackexchange.com/a/9152/3847
    function uint256ToString(uint256 y) private pure returns (string) {
        bytes32 x = bytes32(y);
        bytes memory bytesString = new bytes(32);
        uint charCount = 0;
        for (uint j = 0; j < 32; j++) {
            byte char = byte(bytes32(uint(x) * 2 ** (8 * j)));
            if (char != 0) {
                bytesString[charCount] = char;
                charCount++;
            }
        }
        bytes memory bytesStringTrimmed = new bytes(charCount);
        for (j = 0; j < charCount; j++) {
            bytesStringTrimmed[j] = bytesString[j];
        }
        return string(bytesStringTrimmed);
    }

    function stringToBytes32(string memory source) private pure returns (bytes32 result) {
        bytes memory testEmptyStringTest = bytes(source);
        if (testEmptyStringTest.length == 0) {
            return 0x0;
        }

        assembly {
            result := mload(add(source, 32))
        }
    }

    // https://ethereum.stackexchange.com/questions/729/how-to-concatenate-strings-in-solidity
    function strConcat(string _a, string _b) private pure returns (string) {
        bytes memory bytes_a = bytes(_a);
        bytes memory bytes_b = bytes(_b);
        string memory ab = new string (bytes_a.length + bytes_b.length);
        bytes memory bytes_ab = bytes(ab);
        uint k = 0;
        for (uint i = 0; i < bytes_a.length; i++) bytes_ab[k++] = bytes_a[i];
        for (i = 0; i < bytes_b.length; i++) bytes_ab[k++] = bytes_b[i];
        return string(bytes_ab);
    }

    // TODO: This should be a separate library: OLC.sol

    bytes20 constant DIGITS = bytes20('23456789CFGHJMPQRVWX');
    bytes20 constant STIGID = bytes20('XWVRQPMJHGFC98765432');

    function nextChar(byte c, bytes20 digits) private pure returns (byte) {
        for (uint i = 0; i < 20; i++)
            if (c == digits[i])
                return (i == 19) ? digits[0] : digits[i+1];

    }

    function replaceChar(uint256 tokenId, uint pos, byte c) private pure returns (uint256) {

        uint shift = (8 - pos) * 8;
        uint256 insert = uint256(c) << shift;
        uint256 mask = ~(uint256(0xff) << shift);

        return tokenId & mask | insert;
    }

    function incrementChar(uint256 tokenId, int pos, bytes20 digits) private pure returns (uint256) {

        if (pos < 0)
            return tokenId;

        byte c = nextChar(bytes32(tokenId)[23 + uint(pos)], digits);
        uint256 updated = replaceChar(tokenId, uint(pos), c);
        if (c == digits[0]) {
            int nextPos = pos - 2;
            byte nextPosChar = bytes32(updated)[23 + uint(nextPos)];
            // Longitude has only 18 slices.
            if (nextPos == 1) {
                if (digits == DIGITS && nextPosChar == 'V') {
                    return replaceChar(updated, uint(nextPos), '2');
                }
                if (digits == STIGID && nextPosChar == '2') {
                    return replaceChar(updated, uint(nextPos), 'V');
                }
            }
            // Latitude has only 9 slices.
            if (nextPos == 0) {
                if (digits == DIGITS && nextPosChar == 'C') {
                    return replaceChar(updated, uint(nextPos), '2');
                }
                if (digits == STIGID && nextPosChar == '2') {
                    return replaceChar(updated, uint(nextPos), 'C');
                }
            }
            return incrementChar(updated, nextPos, digits);
        }
        return updated;
    }

    function northOfToken(uint256 tokenId) public pure returns (uint256) {
        return incrementChar(tokenId, 6, DIGITS);
    }

    function southOfToken(uint256 tokenId) public pure returns (uint256) {
        return incrementChar(tokenId, 6, STIGID);
    }

    function eastOfToken(uint256 tokenId) public pure returns (uint256) {
        return incrementChar(tokenId, 7, DIGITS);
    }

    function westOfToken(uint256 tokenId) public pure returns (uint256) {
        return incrementChar(tokenId, 7, STIGID);
    }

    function neighborsOfToken(uint256 tokenId) public pure returns (uint256[4]) {
        return [
            northOfToken(tokenId),
            eastOfToken(tokenId),
            southOfToken(tokenId),
            westOfToken(tokenId)
        ];
    }

}
/* The latest stable Truffle is Truffle 4, which also uses Solidity 4. The highest OpenZeppelin that is compatible with Solidity 4 is 2.0.0. */
function uint256ToString(uint256 y) private pure returns (string) {
    bytes32 x = bytes32(y);
    bytes memory bytesString = new bytes(32);
    uint charCount = 0;
    for (uint j = 0; j < 32; j++) {
        byte char = byte(bytes32(uint(x) * 2 ** (8 * j)));
        if (char != 0) {
            bytesString[charCount] = char;
            charCount++;
        }
    }
    bytes memory bytesStringTrimmed = new bytes(charCount);
    for (j = 0; j < charCount; j++) {
        bytesStringTrimmed[j] = bytesString[j];
    }
    return string(bytesStringTrimmed);
}

// https://ethereum.stackexchange.com/a/9152/3847