contract BigIoAdSpace is Ownable {
    using SafeMath for uint256;

    /// @notice Token
    struct Token {
        uint256 id;
        uint256 x; // position X on map
        uint256 y; // position Y on map
        uint256 sizeA;
        uint256 sizeB;
        uint256 soldPrice; // price that user paid to buy token
        uint256 actualPrice;
        uint256 timesSold; // how many times token was sold
        uint256 timesUpdated; // how many times artwork has been changed by current owner
        uint256 soldAt; // when token was sold
        uint256 inner;
        uint256 outer;
        uint256 soldNearby;
    }

    struct MetaData {
        string meta;
    }

    struct InnerScope {
        uint256 x1; // left top
        uint256 y1;
        uint256 x2; // right top
        uint256 y2;
        uint256 x3; // left bottom
        uint256 y3;
        uint256 x4; // right bottom
        uint256 y4;
    }

    InnerScope public innerScope;

    /// @notice mapping for token URIs
    mapping(uint256 => MetaData) public metadata;

    /// @notice Mapping from token ID to owner
    mapping(uint256 => address) public tokenOwner;

    mapping(uint256 => mapping(uint256 => bool)) public neighbours;
    mapping(uint256 => uint256[]) public neighboursArea;

    /// @notice Here different from base class we store the token not an id
    /// Array with all token, used for enumeration
    Token[] public allMinedTokens;

    /// @notice Mapping from token id to position in the allMinedTokens array
    mapping(uint256 => uint256) public allTokensIndex;

    // store sold units and not-sold but generated units
//    mapping(uint256 => mapping(uint256 => bool)) public soldUnits;
    mapping(uint256 => mapping(uint256 => uint256)) public soldUnits;

    address public platform;

    event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
    event TokenPriceIncreased(uint256 _tokenId, uint256 _newPrice, uint256 _boughtTokenId, uint256 time);

    constructor () public {
        innerScope = InnerScope(
            12, 11, // left top
            67, 11, // right top
            12, 34, // left bottom
            67, 34
        );
    }

    modifier onlyPlatform() {
        require(msg.sender == platform);
        _;
    }

    modifier exists(uint256 _tokenId) {
        address owner = tokenOwner[_tokenId];
        require(owner != address(0));
        _;
    }

    function setPlatform(address _platform) public onlyOwner() {
        platform = _platform;
    }

    function totalSupply() public view returns (uint256) {
        return allMinedTokens.length;
    }

    /// @notice Check whether token is minted
    function tokenExists(uint256 _tokenId) public view returns (bool) {
        address owner = tokenOwner[_tokenId];
        return owner != address(0);
    }

    /// @notice Check whether exist Unit with same x any y coordinates
    /// and it was sold already
    /// in order to prevent over writing
    function unitExists(uint x, uint y) public view returns (bool) {
        return (soldUnits[x][y] != 0);
    }

    function getOwner(uint256 _tokenId) public view returns (address) {
        return tokenOwner[_tokenId];
    }

    /// @return token metadata
    function getMetadata(uint256 _tokenId) public exists(_tokenId) view returns (string) {
        return metadata[_tokenId].meta;
    }

    /// @notice update metadata for token
    function setTokenMetadata(uint256 _tokenId, string meta) public  onlyPlatform exists(_tokenId) {
        metadata[_tokenId] = MetaData(meta);
    }

    function increaseUpdateMetadataCounter(uint256 _tokenId) public onlyPlatform {
        allMinedTokens[allTokensIndex[_tokenId]].timesUpdated = allMinedTokens[allTokensIndex[_tokenId]].timesUpdated.add(1);
    }

    /// @notice remove metadata for token
    function removeTokenMetadata(uint256 _tokenId) public onlyPlatform exists(_tokenId) {
        delete metadata[_tokenId];
    }

    // @return return the current price
    function getCurrentPriceForToken(uint256 _tokenId) public exists(_tokenId) view returns (uint256) {
        return allMinedTokens[allTokensIndex[_tokenId]].actualPrice;
    }

    /// @return tokenId, x, y, sizeA, sizeB, price, inner, outer
    function getTokenData(uint256 _tokenId) public exists(_tokenId) view returns(uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256) {
        Token memory token = allMinedTokens[allTokensIndex[_tokenId]];
        return (_tokenId, token.x, token.y, token.sizeA, token.sizeB, token.actualPrice, token.soldPrice, token.inner, token.outer);
    }

    function getTokenSoldPrice(uint256 _tokenId) public exists(_tokenId) view returns (uint256) {
        Token memory token = allMinedTokens[allTokensIndex[_tokenId]];
        return token.soldPrice;
    }

    function getTokenUpdatedCounter(uint256 _tokenId) public exists(_tokenId) view returns (uint256) {
        return allMinedTokens[allTokensIndex[_tokenId]].timesUpdated;
    }

    // @return return token sizes
    function getTokenSizes(uint256 _tokenId) public exists(_tokenId) view returns (uint256, uint256) {
        Token memory token = allMinedTokens[allTokensIndex[_tokenId]];
        return (token.sizeA, token.sizeB);
    }

    // @return return token scopes
    function getTokenScope(uint256 _tokenId) public exists(_tokenId) view returns (bool, bool) {
        Token memory token = allMinedTokens[allTokensIndex[_tokenId]];
        return (token.inner > 0, token.outer > 0);
    }

    // @return return token scope counters
    function getTokenCounters(uint256 _tokenId) public exists(_tokenId) view returns (uint256, uint256, uint256, uint256) {
        Token memory token = allMinedTokens[allTokensIndex[_tokenId]];
        return (token.inner, token.outer, token.timesSold, token.soldNearby);
    }

    /// @notice Mint new token, not sell new token
    /// BE sends: owner, x coordinate, y coordinate, price
    /// @param to new owner
    /// @param x coordinate
    /// @param y coordinate
    /// @param totalPrice calculated price for all units + % for siblings
    function mint(
        address to,
        uint x,
        uint y,
        uint sizeA,
        uint sizeB,
        uint256 totalPrice,
        uint256 actualPrice
    ) public onlyPlatform() returns (uint256) {

        // 1.
        require(to != address(0));
        require(sizeA.mul(sizeB) <= 100);

        // 2. check area
        uint256 inner;
        uint256 total;

        (total, inner) = calculateCounters(x, y, sizeA, sizeB);

        // we avoid zero because we later compare against zero
        uint256 tokenId = (allMinedTokens.length).add(1);

        // @TODO: ACHTUNG - soldAt equals 0 during minting
        Token memory minted = Token(tokenId, x, y, sizeA, sizeB, totalPrice, actualPrice, 0, 0, 0, inner, total.sub(inner), 0);

        // 3. copy units and create siblings
        copyToAllUnits(x, y, sizeA, sizeB, tokenId);

        // 4. update state
        updateInternalState(minted, to);

        return tokenId;
    }

    function updateTokensState(uint256 _tokenId, uint256 newPrice) public onlyPlatform exists(_tokenId) {
        uint256 index = allTokensIndex[_tokenId];
        allMinedTokens[index].timesSold += 1;
        allMinedTokens[index].timesUpdated = 0;
        allMinedTokens[index].soldNearby = 0;
        allMinedTokens[index].soldPrice = newPrice;
        allMinedTokens[index].actualPrice = newPrice;
        allMinedTokens[index].soldAt = now;
    }

    function updateOwner(uint256 _tokenId, address newOwner, address prevOwner) public onlyPlatform exists(_tokenId) {
        require(newOwner != address(0));
        require(prevOwner != address(0));
        require(prevOwner == tokenOwner[_tokenId]);

        // update data for new owner
        tokenOwner[_tokenId] = newOwner;
    }

    function inInnerScope(uint256 x, uint256 y) public view returns (bool) {
        // x should be between left top and right top corner
        // y should be between left top and left bottom corner
        if ((x >= innerScope.x1) && (x <= innerScope.x2) && (y >= innerScope.y1) && (y <= innerScope.y3)) {
            return true;
        }

        return false;
    }

    function calculateCounters(uint256 x, uint256 y, uint256 sizeA, uint256 sizeB) public view returns (uint256 total, uint256 inner) {
        uint256 upX = x.add(sizeA);
        uint256 upY = y.add(sizeB);

        // check for boundaries
        require(x >= 1);
        require(y >= 1);
        require(upX <= 79);
        require(upY <= 45);
        require(sizeA > 0);
        require(sizeB > 0);

        uint256 i;
        uint256 j;

        for (i = x; i < upX; i++) {
            for (j = y; j < upY; j++) {
                require(soldUnits[i][j] == 0);

                if (inInnerScope(i, j)) {
                    inner = inner.add(1);
                }

                total = total.add(1);
            }
        }
    }

    function increasePriceForNeighbours(uint256 tokenId) public onlyPlatform {

        Token memory token = allMinedTokens[allTokensIndex[tokenId]];

        uint256 upX = token.x.add(token.sizeA);
        uint256 upY = token.y.add(token.sizeB);

        uint256 i;
        uint256 j;
        uint256 k;
        uint256 _tokenId;


        if (neighboursArea[tokenId].length == 0) {

            for (i = token.x; i < upX; i++) {
                // check neighbors on top of area
                _tokenId = soldUnits[i][token.y - 1];

                if (_tokenId != 0) {
                    if (!neighbours[tokenId][_tokenId]) {
                        neighbours[tokenId][_tokenId] = true;
                        neighboursArea[tokenId].push(_tokenId);
                    }
                    if (!neighbours[_tokenId][tokenId]) {
                        neighbours[_tokenId][tokenId] = true;
                        neighboursArea[_tokenId].push(tokenId);
                    }
                }

                // check neighbors on bottom of area
                _tokenId = soldUnits[i][upY];
                if (_tokenId != 0) {
                    if (!neighbours[tokenId][_tokenId]) {
                        neighbours[tokenId][_tokenId] = true;
                        neighboursArea[tokenId].push(_tokenId);
                    }
                    if (!neighbours[_tokenId][tokenId]) {
                        neighbours[_tokenId][tokenId] = true;
                        neighboursArea[_tokenId].push(tokenId);
                    }
                }
            }

            for (j = token.y; j < upY; j++) {
                // check neighbors on left of area of area
                _tokenId = soldUnits[token.x - 1][j];
                if (_tokenId != 0) {
                    if (!neighbours[tokenId][_tokenId]) {
                        neighbours[tokenId][_tokenId] = true;
                        neighboursArea[tokenId].push(_tokenId);
                    }
                    if (!neighbours[_tokenId][tokenId]) {
                        neighbours[_tokenId][tokenId] = true;
                        neighboursArea[_tokenId].push(tokenId);
                    }
                }

                // check neighbors on right of area of area
                _tokenId = soldUnits[upX][j];
                if (_tokenId != 0) {
                    if (!neighbours[tokenId][_tokenId]) {
                        neighbours[tokenId][_tokenId] = true;
                        neighboursArea[tokenId].push(_tokenId);
                    }
                    if (!neighbours[_tokenId][tokenId]) {
                        neighbours[_tokenId][tokenId] = true;
                        neighboursArea[_tokenId].push(tokenId);
                    }
                }
            }
        }

        // increase price
        for (k = 0; k < neighboursArea[tokenId].length; k++) {
            Token storage _token = allMinedTokens[allTokensIndex[neighboursArea[tokenId][k]]];
            _token.soldNearby = _token.soldNearby.add(1);
            _token.actualPrice = _token.actualPrice.add((_token.actualPrice.div(100)));
            emit TokenPriceIncreased(_token.id, _token.actualPrice, _tokenId, now);
        }
    }

    // move generated Units to sold array
    // generate siblings and put it there
    function copyToAllUnits(uint256 x, uint256 y, uint256 width, uint256 height, uint256 tokenId) internal {
        uint256 upX = x + width; // 5
        uint256 upY = y + height; // 3

        uint256 i; // 1
        uint256 j; // 1

        for (i = x; i < upX; i++) {
            for (j = y; j < upY; j++) {
                soldUnits[i][j] = tokenId;
            }
        }
    }

    function updateInternalState(Token minted, address _to) internal {
        uint256 lengthT = allMinedTokens.length;
        allMinedTokens.push(minted);
        allTokensIndex[minted.id] = lengthT;
        tokenOwner[minted.id] = _to;
    }
}
function setTokenMetadata(uint256 _tokenId, string meta) public  onlyPlatform exists(_tokenId) {
    metadata[_tokenId] = MetaData(meta);
}

/// @notice update metadata for token