contract Platform is Admin {

    using SafeMath for uint256;

    struct Offer {
        uint256 tokenId;
        uint256 offerId;
        address from;
        uint256 offeredPrice;
        uint256 tokenPrice;
        bool accepted;
        uint256 timestamp;
    }

    struct ArtWorkChangeRequest {
        address fromUser;
        uint256 tokenId;
        uint256 changeId;
        string meta;
        uint256 timestamp;
        bool isReviewed;
    }

    BigIoAdSpace public token;
    BigIOERC20token public erc20token;

    PricingStrategy public pricingStrategy;
    ArtWorkChangeRequest[] public artWorkChangeRequests;

    bool public isLogoInitied;

    uint256 public logoX = 35;
    uint256 public logoY = 18;

    Offer[] public offers;

    mapping(address => uint256) public pendingReturns;

    event Minted(address indexed _owner, uint256 _tokenId, uint256 _x, uint256 _y, uint256 _sizeA, uint256 _sizeB, uint256 _price, uint256 _platformTransfer, uint256 _timestamp);

    event Purchased(address indexed _from, address indexed _to, uint256 _tokenId, uint256 _price, uint256 _prevPrice, uint256 _prevOwnerTransfer, uint256 _platformTransfer, uint256 _timestamp);

    event OfferMade(address indexed _fromUser, uint256 _tokenId, uint256 _offerId, uint256 _offeredPrice, uint256 _timestamp);

    event OfferApproved(address indexed _owner, uint256 _tokenId, uint256 _offerId, uint256 _offeredPrice, uint256 _timestamp);

    event OfferDeclined(address indexed _fromUser, uint256 _tokenId, uint256 _offerId, uint256 _offeredPrice, uint256 _timestamp);

    event ArtWorkChangeRequestMade(
        address indexed _fromUser,
        uint256 _tokenId,
        uint256 _changeId,
        string _meta,
        uint256 _platformTransfer,
        uint256 _timestamp);

    event ArtWorkChangeRequestApproved(
        address indexed _fromUser,
        uint256 _tokenId,
        uint256 _changeId,
        string _meta,
        uint256 _timestamp);

    event ArtWorkChangeRequestDeclined(
        address indexed _fromUser,
        uint256 _tokenId,
        uint256 _changeId,
        string _meta,
        uint256 _timestamp);

    event RemovedMetaData(uint256 _tokenId, address _admin, string _meta, uint256 _timestamp);
    event ChangedOwnership(uint256 _tokenId, address _prevOwner, address _newOwner, uint256 _timestamp);

    constructor(
        address _platformWallet, // owner collects money
        address _token,
        address _erc20token,
        address _pricingStrategy,
        address _signer
    ) public {

        token = BigIoAdSpace(_token);
        erc20token = BigIOERC20token(_erc20token);

        platformWallet = _platformWallet;

        pricingStrategy = PricingStrategy(_pricingStrategy);

        signers[_signer] = true;

        // 30%
        tiers.push(
            Tier(
                400, // amountInCenter
                600, // amountInOuter
                1 ether, // priceInCenter
                0.4 ether, // priceInOuter
                0, // soldInCenter
                0, // soldInOuter
                false, // filledInCenter
                false // filledInOuter
            )
        );
        // 30%
        tiers.push(
            Tier(
                400, 600, 1.2 ether, 0.6 ether, 0, 0, false, false
            )
        );
        // 30%
        tiers.push(
            Tier(
                400, 600, 1.4 ether, 0.8 ether, 0, 0, false, false
            )
        );
        // 10%
        tiers.push(
            Tier(
                144, 288, 1.6 ether, 1.0 ether, 0, 0, false, false
            )
        );
    }

    /// @notice init logo, call it as soon as possible
    /// call it after setting platform in the token
    /// Logo is BigIOToken which has 10*10 size and position in the center of map
    function initLogo() public onlyOwner {
        require(isLogoInitied == false);

        isLogoInitied = true;

        logoId = token.mint(platformWallet, logoX, logoY, 10, 10, 0 ether, 0 ether);

        token.setTokenMetadata(logoId, "");

        updateTierStatus(100, 0);

        emit Minted(msg.sender, logoId, logoX, logoY, 10, 10, 0 ether, 0 ether, now);
    }

    function getPriceFor(uint256 x, uint256 y, uint256 sizeA, uint256 sizeB) public view returns(uint256 totalPrice, uint256 inner, uint256 outer) {
        (inner, outer) = preMinting(x, y, sizeA, sizeB);

        totalPrice = calculateTokenPrice(inner, outer);

        return (totalPrice, inner, outer);
    }

    /// @notice sell new tokens during the round 0
    /// all except logo
    function buy(
        uint256 x, // top left coordinates
        uint256 y, // top left coordinates
        uint256 sizeA, // size/width of a square
        uint256 sizeB, // size/height of a square,
        uint8 _v,  // component of signature
        bytes32 _r, // component of signature
        bytes32 _s // component of signature
    ) public notHalted() payable {
        address recoveredSigner = verify(keccak256(msg.sender), _v, _r, _s);

        require(signers[recoveredSigner] == true);
        require(msg.value > 0);

        internalBuy(x, y, sizeA, sizeB);
    }

    function internalBuy(
        uint256 x, // top left coordinates
        uint256 y, // top left coordinates
        uint256 sizeA, // size/width of a square
        uint256 sizeB // size/height of a square,
    ) internal {
        // get and validate current tier
        uint256 inner = 0;
        uint256 outer = 0;
        uint256 totalPrice = 0;

        (inner, outer) = preMinting(x, y, sizeA, sizeB);
        totalPrice = calculateTokenPrice(inner, outer);

        require(totalPrice <= msg.value);

        //         try to mint and update current tier
        updateTierStatus(inner, outer);

        uint256 actualPrice = inner.mul(tiers[3].priceInCenter).add(outer.mul(tiers[3].priceInOuter));

        if (msg.value > actualPrice) {
            actualPrice = msg.value;
        }

        uint256 tokenId = token.mint(msg.sender, x, y, sizeA, sizeB, msg.value, actualPrice);
        erc20token.mint(msg.sender, inner.add(outer));

        transferEthers(platformWallet, msg.value);

        emit Minted(msg.sender, tokenId, x, y, sizeA, sizeB, msg.value, msg.value, now);
    }

    /// @notice allow user to make an offer after initial phase(re-sale)
    /// any offer minResalePercentage is accepted automatically
    function makeOffer(
        uint256 _tokenId,
        uint8 _v,  // component of signature
        bytes32 _r, // component of signature
        bytes32 _s // component of signature
    ) public notHalted() payable {

        address recoveredSigner = verify(keccak256(msg.sender), _v, _r, _s);

        require(signers[recoveredSigner] == true);

        require(msg.sender != address(0));
        require(msg.value > 0);

        uint256 currentPrice = getTokenPrice(_tokenId);
        require(currentPrice > 0);

        // special case for first sell of logo
        if (_tokenId == logoId && token.getCurrentPriceForToken(_tokenId) == 0) {
            require(msg.value >= logoPrice);

            //update token's state
            token.updateTokensState(logoId, msg.value);

            // mint erc20 tokens
            erc20token.mint(msg.sender, 100);

            transferEthers(platformWallet, msg.value);

            emit Purchased(0, msg.sender, _tokenId, msg.value, 0, 0, msg.value, now);

            return;
        }

        uint256 minPrice = pricingStrategy.calculateMinPriceForNextRound(currentPrice, minResalePercentage);

        require(msg.value >= minPrice);

        uint256 offerCounter = offers.length;

        offers.push(Offer(_tokenId, offerCounter, msg.sender, msg.value, currentPrice, false, now));
        emit OfferMade(msg.sender, _tokenId, offerCounter, msg.value, now);

        // 2. check condition for approve and do it
        approve(offerCounter, _tokenId);
    }

    function getTokenPrice(uint256 _tokenId) public view returns (uint256 price) {

        uint256 actualPrice = token.getCurrentPriceForToken(_tokenId);

        // special case for first sell of logo
        if (_tokenId == logoId && actualPrice == 0) {
            require(logoPrice > 0);

            return logoPrice;
        } else {
            uint256 indexInner = 0;
            uint256 indexOuter = 0;

            bool hasInner;
            bool hasOuter;

            (hasInner, hasOuter) = token.getTokenScope(_tokenId);
            (indexInner, indexOuter) = getCurrentTierIndex();

            if (_tokenId != logoId && hasInner) {
                require(indexInner == 100000);
            }

            if (hasOuter) {
                require(indexOuter == 100000);
            }

            return actualPrice;
        }
    }

    function getArtWorkChangeFee(uint256 _tokenId) public view returns (uint256 fee) {

        uint256 counter = token.getTokenUpdatedCounter(_tokenId);

        if (counter > 0) {
            return feeForArtWorkChangeRequest;
        }

        return feeForFirstArtWorkChangeRequest;
    }

    /// @notice it allows token owner to create art work change request
    /// first user upload 2 images
    /// then do call this function
    /// admin can reject or approve it
    function artWorkChangeRequest(uint256 _tokenId, string _meta, uint8 _v, bytes32 _r, bytes32 _s)
        public payable returns (uint256)
    {

        address recoveredSigner = verify(keccak256(_meta), _v, _r, _s);

        require(signers[recoveredSigner] == true);

        require(msg.sender == token.getOwner(_tokenId));

        uint256 fee = getArtWorkChangeFee(_tokenId);

        require(msg.value >= fee);

        uint256 changeRequestCounter = artWorkChangeRequests.length;

        artWorkChangeRequests.push(
            ArtWorkChangeRequest(msg.sender, _tokenId, changeRequestCounter, _meta, now, false)
        );

        token.increaseUpdateMetadataCounter(_tokenId);

        transferEthers(platformWallet, msg.value);

        emit ArtWorkChangeRequestMade(msg.sender, _tokenId, changeRequestCounter, _meta, msg.value, now);

        return changeRequestCounter;
    }

    function artWorkChangeApprove(uint256 _index, uint256 _tokenId, bool approve) public onlyAdmin {
        ArtWorkChangeRequest storage request = artWorkChangeRequests[_index];
        require(false == request.isReviewed);

        require(_tokenId == request.tokenId);
        request.isReviewed = true;
        if (approve) {
            token.setTokenMetadata(_tokenId, request.meta);
            emit ArtWorkChangeRequestApproved(
                request.fromUser,
                request.tokenId,
                request.changeId,
                request.meta,
                now
            );
        } else {
            emit ArtWorkChangeRequestDeclined(
                request.fromUser,
                request.tokenId,
                request.changeId,
                request.meta,
                now
            );
        }
    }

    function artWorkChangeByAdmin(uint256 _tokenId, string _meta, uint256 _changeId) public onlyAdmin {
        token.setTokenMetadata(_tokenId, _meta);
        emit ArtWorkChangeRequestApproved(
            msg.sender,
            _tokenId,
            _changeId,
            _meta,
            now
        );
    }

    function changeTokenOwnerByAdmin(uint256 _tokenId, address _newOwner) public onlyAdmin {
        address prevOwner = token.getOwner(_tokenId);
        token.updateOwner(_tokenId, _newOwner, prevOwner);
        emit ChangedOwnership(_tokenId, prevOwner, _newOwner, now);
        string memory meta = token.getMetadata(_tokenId);
        token.removeTokenMetadata(_tokenId);
        emit RemovedMetaData(_tokenId, msg.sender, meta, now);
    }

    /// @return tokenId, x, y, sizeA, sizeB, price
    function getTokenData(uint256 _tokenId) public view returns(uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256) {
        return token.getTokenData(_tokenId);
    }

    function getMetaData(uint256 _tokenId) public view returns(string) {
        return token.getMetadata(_tokenId);
    }

    /// @notice Withdraw a bid that was overbid and platform owner share
    function claim() public returns (bool) {
        return claimInternal(msg.sender);
    }

    /// @notice Withdraw (for cold storage wallet) a bid that was overbid and platform owner share
    function claimByAddress(address _address) public returns (bool) {
        return claimInternal(_address);
    }

    function claimInternal(address _address) internal returns (bool) {
        require(_address != address(0));

        uint256 amount = pendingReturns[_address];

        if (amount == 0) {
            return;
        }

        pendingReturns[_address] = 0;

        _address.transfer(amount);

        return true;
    }

    /// @return index of current tiers, if 100000(cannot use -1) then round 0  finished
    /// and we need to move to re-sale
    function getCurrentTierIndex() public view returns (uint256, uint256) {
        // cannot use -1
        // so use not possible value for tiers.length
        uint256 indexInner = 100000;
        uint256 indexOuter = 100000;

        for (uint256 i = 0; i < tiers.length; i++) {
            if (!tiers[i].filledInCenter) {
                indexInner = i;
                break;
            }
        }

        for (uint256 k = 0; k < tiers.length; k++) {
            if (!tiers[k].filledInOuter) {
                indexOuter = k;
                break;
            }
        }

        return (indexInner, indexOuter);
    }

    /// @return current tier stats
    /// index of current tiers, if 100000(cannot use -1) then initial sale is  finished
    /// works only during the initial phase
    function getCurrentTierStats() public view returns (uint256 indexInner, uint256 indexOuter, uint256 availableInner, uint256 availableInOuter, uint256 priceInCenter, uint256 priceInOuter, uint256 nextPriceInCenter, uint256 nextPriceInOuter) {

        indexInner = 100000;
        indexOuter = 100000;

        for (uint256 i = 0; i < tiers.length; i++) {
            if (!tiers[i].filledInCenter) {
                indexInner = i;
                break;
            }
        }

        for (uint256 k = 0; k < tiers.length; k++) {
            if (!tiers[k].filledInOuter) {
                indexOuter = k;
                break;
            }
        }

        Tier storage tier;

        if (indexInner != 100000) {
            tier = tiers[indexInner];

            availableInner = tier.amountInCenter.sub(tier.soldInCenter);

            priceInCenter = tier.priceInCenter;

            if (indexInner < 3) {
                nextPriceInCenter = tiers[indexInner + 1].priceInCenter;
            }
        }

        if (indexOuter != 100000) {
            tier = tiers[indexOuter];

            availableInOuter = tier.amountInOuter.sub(tier.soldInOuter);

            priceInOuter = tier.priceInOuter;

            if (indexOuter < 3) {
                nextPriceInOuter = tiers[indexOuter + 1].priceInOuter;
            }
        }
    }

    function calculateAmountOfUnits(uint256 sizeA, uint256 sizeB) public pure returns (uint256) {
        return sizeA.mul(sizeB);
    }

    /// @notice approve the offer
    function approve(uint256 _index, uint256 _tokenId) internal {
        Offer memory localOffer = offers[_index];

        address newOwner = localOffer.from;
        address prevOwner = token.getOwner(_tokenId);

        uint256 platformShare;
        uint256 forPrevOwner;

        uint256 soldPrice = token.getTokenSoldPrice(_tokenId);

        (platformShare, forPrevOwner) = pricingStrategy.calculateSharesInTheRevenue(
            soldPrice, localOffer.offeredPrice);

        //update token's state
        token.updateTokensState(_tokenId, localOffer.offeredPrice);

        // update owner
        token.updateOwner(_tokenId, newOwner, prevOwner);
        localOffer.accepted = true;

        transferEthers(platformWallet, platformShare);
        transferEthers(prevOwner, forPrevOwner.add(soldPrice));

        emit OfferApproved(newOwner, _tokenId, localOffer.offerId, localOffer.offeredPrice, now);
        emit Purchased(prevOwner, newOwner, _tokenId, localOffer.offeredPrice, soldPrice, forPrevOwner.add(soldPrice), platformShare, now);

        afterApproveAction(_tokenId);
    }

    function transferEthers(address _address, uint256 _wei) internal {
        if (isContract(_address)) {
            pendingReturns[_address] = pendingReturns[_address].add(_wei);
        }
        else {
            _address.transfer(_wei);
        }
    }

    function preMinting(uint256 x, uint256 y, uint256 sizeA, uint256 sizeB) internal view returns (uint256, uint256) {
        //  calculate units for token
        uint256 total = 0;
        uint256 inner = 0;
        uint256 outer = 0;


        (total, inner) = token.calculateCounters(x, y, sizeA, sizeB);
        outer = total.sub(inner);

        require(total <= 100);

        return (inner, outer);
    }

    function updateTierStatus(uint256 inner, uint256 outer) internal {
        uint256 leftInner = inner;
        uint256 leftOuter = outer;

        for (uint256 i = 0; i < 4; i++) {
            Tier storage tier = tiers[i];

            if (leftInner > 0 && tier.filledInCenter == false) {
                uint256 availableInner = tier.amountInCenter.sub(tier.soldInCenter);

                if (availableInner > leftInner) {
                    tier.soldInCenter = tier.soldInCenter.add(leftInner);

                    leftInner = 0;
                } else {
                    tier.filledInCenter = true;
                    tier.soldInCenter = tier.amountInCenter;

                    leftInner = leftInner.sub(availableInner);
                }
            }

            if (leftOuter > 0 && tier.filledInOuter == false) {
                uint256 availableOuter = tier.amountInOuter.sub(tier.soldInOuter);

                if (availableOuter > leftOuter) {
                    tier.soldInOuter = tier.soldInOuter.add(leftOuter);

                    leftOuter = 0;
                } else {
                    tier.filledInOuter = true;
                    tier.soldInOuter = tier.amountInOuter;

                    leftOuter = leftOuter.sub(availableOuter);
                }
            }
        }

        require(leftInner == 0 && leftOuter == 0);
    }

    function calculateTokenPrice(uint256 inner, uint256 outer) public view returns (uint256 price) {
        uint256 leftInner = inner;
        uint256 leftOuter = outer;

        for (uint256 i = 0; i < 4; i++) {
            Tier storage tier = tiers[i];

            if (leftInner > 0 && tier.filledInCenter == false) {
                uint256 availableInner = tier.amountInCenter.sub(tier.soldInCenter);

                if (availableInner > leftInner) {
                    price = price.add(leftInner.mul(tier.priceInCenter));
                    leftInner = 0;
                } else {
                    price = price.add(availableInner.mul(tier.priceInCenter));
                    leftInner = leftInner.sub(availableInner);
                }
            }

            if (leftOuter > 0 && tier.filledInOuter == false) {
                uint256 availableOuter = tier.amountInOuter.sub(tier.soldInOuter);

                if (availableOuter > leftOuter) {
                    price = price.add(leftOuter.mul(tier.priceInOuter));
                    leftOuter = 0;
                } else {
                    price = price.add(availableOuter.mul(tier.priceInOuter));
                    leftOuter = leftOuter.sub(availableOuter);
                }
            }
        }

        require(leftInner == 0 && leftOuter == 0);
    }

    function minPriceForNextRound(uint256 _tokenId) public view returns (uint256) {
        if (_tokenId == logoId && token.getCurrentPriceForToken(_tokenId) == 0) {
            return logoPrice;
        } else {
            // @TODO: check if sold-out

            uint256 currentPrice = getTokenPrice(_tokenId);
            uint256 minPrice = pricingStrategy.calculateMinPriceForNextRound(currentPrice, minResalePercentage);
            return minPrice;
        }
    }

    function afterApproveAction(uint256 _tokenId) internal {

        uint256 indexInner = 100000;
        uint256 indexOuter = 100000;

        bool hasInner;
        bool hasOuter;

        (hasInner, hasOuter) = token.getTokenScope(_tokenId);
        (indexInner, indexOuter) = getCurrentTierIndex();

        if (hasInner && hasOuter && indexInner == 100000 && indexOuter == 100000) {
            token.increasePriceForNeighbours(_tokenId);
        } else if (!hasInner && hasOuter && indexOuter == 100000) {
            token.increasePriceForNeighbours(_tokenId);
        } else if (!hasOuter && hasInner && indexInner == 100000) {
            token.increasePriceForNeighbours(_tokenId);
        }
    }

    function canBuyExistentToken(uint256 _tokenId) public view returns (uint256 _allowed) {
        uint256 indexInner = 0;
        uint256 indexOuter = 0;

        bool hasInner;
        bool hasOuter;

        (hasInner, hasOuter) = token.getTokenScope(_tokenId);
        (indexInner, indexOuter) = getCurrentTierIndex();

        if (token.getCurrentPriceForToken(_tokenId) == 0 && logoPrice == 0) {
            return 4;
        }

        if (_tokenId != logoId && hasInner && indexInner != 100000) {
            return 2;
        }

        if (hasOuter && indexOuter != 100000) {
            return 3;
        }

        return 1;
    }
}
/// @title Platform
/// @author Applicature
/// @notice It is front contract which is used to sell, re-sell tokens and initiate paying dividends
function artWorkChangeRequest(uint256 _tokenId, string _meta, uint8 _v, bytes32 _r, bytes32 _s)
    public payable returns (uint256)
{

    address recoveredSigner = verify(keccak256(_meta), _v, _r, _s);

    require(signers[recoveredSigner] == true);

    require(msg.sender == token.getOwner(_tokenId));

    uint256 fee = getArtWorkChangeFee(_tokenId);

    require(msg.value >= fee);

    uint256 changeRequestCounter = artWorkChangeRequests.length;

    artWorkChangeRequests.push(
        ArtWorkChangeRequest(msg.sender, _tokenId, changeRequestCounter, _meta, now, false)
    );

    token.increaseUpdateMetadataCounter(_tokenId);

    transferEthers(platformWallet, msg.value);

    emit ArtWorkChangeRequestMade(msg.sender, _tokenId, changeRequestCounter, _meta, msg.value, now);

    return changeRequestCounter;
}

/// @notice it allows token owner to create art work change request
/// first user upload 2 images
/// then do call this function
/// admin can reject or approve it