contract CCTimeSaleManager is ERC721Holder, OperationalControl {
    //DATATYPES & CONSTANTS
    struct CollectibleSale {
        // Current owner of NFT (ERC721)
        address seller;
        // Price (in wei) at beginning of sale (For Buying)
        uint256 startingPrice;
        // Price (in wei) at end of sale (For Buying)
        uint256 endingPrice;
        // Duration (in seconds) of sale, 2592000 = 30 days
        uint256 duration;
        // Time when sale started
        // NOTE: 0 if this sale has been concluded
        uint64 startedAt;
        // Flag denoting is the Sale still active
        bool isActive;
        // address of the wallet who bought the asset
        address buyer;
        // ERC721 AssetID
        uint256 tokenId;
    }
    struct PastSales {
        uint256[5] sales;
    }

    // CCNTFAddress
    address public NFTAddress;

    // Map from token to their corresponding sale.
    mapping (uint256 => CollectibleSale) public tokenIdToSale;

    // Count of AssetType Sales
    mapping (uint256 => uint256) public assetTypeSaleCount;

    // Last 5 Prices of AssetType Sales
    mapping (uint256 => PastSales) internal assetTypeSalePrices;

    uint256 public avgSalesToCount = 5;

    // type to sales of type
    mapping(uint256 => uint256[]) public assetTypeSalesTokenId;

    event SaleWinner(address owner, uint256 collectibleId, uint256 buyingPrice);
    event SaleCreated(uint256 tokenID, uint256 startingPrice, uint256 endingPrice, uint256 duration, uint64 startedAt);
    event SaleCancelled(address seller, uint256 collectibleId);

    // If > 0 then vending is enable for item type
    mapping (uint256 => uint256) internal vendingAmountType;

    // If > 0 then vending is enable for item type
    mapping (uint256 => uint256) internal vendingTypeSold;

    // Current Price for vending type 
    mapping (uint256 => uint256) internal vendingPrice;

    // Price to step up for vending type
    mapping (uint256 => uint256) internal vendingStepUpAmount;

    // Qty to step up vending item
    mapping (uint256 => uint256) internal vendingStepUpQty;

    uint256 public startingIndex = 100000;

    uint256 public vendingAttachedState = 1;


    constructor() public {
        require(msg.sender != address(0));
        paused = true;
        error = false;
        managerPrimary = msg.sender;
        managerSecondary = msg.sender;
        bankManager = msg.sender;
    }

    function  setNFTAddress(address _address) public onlyManager {
        NFTAddress = _address;
    }

    function setAvgSalesCount(uint256 _count) public onlyManager  {
        avgSalesToCount = _count;
    }

    /// @dev Creates and begins a new sale.
    function CreateSale(uint256 _tokenId, uint256 _startingPrice, uint256 _endingPrice, uint64 _duration, address _seller) public anyOperator {
        _createSale(_tokenId, _startingPrice, _endingPrice, _duration, _seller);
    }

    function BatchCreateSales(uint256[] _tokenIds, uint256 _startingPrice, uint256 _endingPrice, uint64 _duration, address _seller) public anyOperator {
        uint256 _tokenId;
        for (uint256 i = 0; i < _tokenIds.length; ++i) {
            _tokenId = _tokenIds[i];
            _createSale(_tokenId, _startingPrice, _endingPrice, _duration, _seller);
        }
    }

    function CreateSaleAvgPrice(uint256 _tokenId, uint256 _margin, uint _minPrice, uint256 _endingPrice, uint64 _duration, address _seller) public anyOperator {
        var ccNFT = CCNFTFactory(NFTAddress);
        uint256 assetType = ccNFT.getAssetIdItemType(_tokenId);
        // Avg Price of last sales
        uint256 salePrice = GetAssetTypeAverageSalePrice(assetType);

        //  0-10,000 is mapped to 0%-100% - will be typically 12000 or 120%
        salePrice = salePrice * _margin / 10000;

        if(salePrice < _minPrice) {
            salePrice = _minPrice;
        } 
       
        _createSale(_tokenId, salePrice, _endingPrice, _duration, _seller);
    }

    function BatchCreateSaleAvgPrice(uint256[] _tokenIds, uint256 _margin, uint _minPrice, uint256 _endingPrice, uint64 _duration, address _seller) public anyOperator {
        var ccNFT = CCNFTFactory(NFTAddress);
        uint256 assetType;
        uint256 _tokenId;
        uint256 salePrice;
        for (uint256 i = 0; i < _tokenIds.length; ++i) {
            _tokenId = _tokenIds[i];
            assetType = ccNFT.getAssetIdItemType(_tokenId);
            // Avg Price of last sales
            salePrice = GetAssetTypeAverageSalePrice(assetType);

            //  0-10,000 is mapped to 0%-100% - will be typically 12000 or 120%
            salePrice = salePrice * _margin / 10000;

            if(salePrice < _minPrice) {
                salePrice = _minPrice;
            } 
            
            _tokenId = _tokenIds[i];
            _createSale(_tokenId, salePrice, _endingPrice, _duration, _seller);
        }
    }

    function BatchCancelSales(uint256[] _tokenIds) public anyOperator {
        uint256 _tokenId;
        for (uint256 i = 0; i < _tokenIds.length; ++i) {
            _tokenId = _tokenIds[i];
            _cancelSale(_tokenId);
        }
    }

    function CancelSale(uint256 _assetId) public anyOperator {
        _cancelSale(_assetId);
    }

    function GetCurrentSalePrice(uint256 _assetId) external view returns(uint256 _price) {
        CollectibleSale memory _sale = tokenIdToSale[_assetId];
        
        return _currentPrice(_sale);
    }

    function GetCurrentTypeSalePrice(uint256 _assetType) external view returns(uint256 _price) {
        CollectibleSale memory _sale = tokenIdToSale[assetTypeSalesTokenId[_assetType][0]];
        return _currentPrice(_sale);
    }

    function GetCurrentTypeDuration(uint256 _assetType) external view returns(uint256 _duration) {
        CollectibleSale memory _sale = tokenIdToSale[assetTypeSalesTokenId[_assetType][0]];
        return  _sale.duration;
    }

    function GetCurrentTypeStartTime(uint256 _assetType) external view returns(uint256 _startedAt) {
        CollectibleSale memory _sale = tokenIdToSale[assetTypeSalesTokenId[_assetType][0]];
        return  _sale.startedAt;
    }

    function GetCurrentTypeSaleItem(uint256 _assetType) external view returns(address seller, uint256 startingPrice, uint256 endingPrice, uint256 duration, uint256 startedAt, uint256 tokenId) {
        CollectibleSale memory _sale = tokenIdToSale[assetTypeSalesTokenId[_assetType][0]];
        return (
            _sale.seller,
            _sale.startingPrice,
            _sale.endingPrice,
            _sale.duration,
            _sale.startedAt,
            _sale.tokenId
        );
    }

    function GetCurrentTypeSaleCount(uint256 _assetType) external view returns(uint256 _count) {
        return assetTypeSalesTokenId[_assetType].length;
    }

    function BuyCurrentTypeOfAsset(uint256 _assetType) external whenNotPaused payable {
        require(msg.sender != address(0));
        require(msg.sender != address(this));

        CollectibleSale memory _sale = tokenIdToSale[assetTypeSalesTokenId[_assetType][0]];
        require(_isOnSale(_sale));

        _buy(_sale.tokenId, msg.sender, msg.value);
    }

    /// @dev BuyNow Function which call the interncal buy function
    /// after doing all the pre-checks required to initiate a buy
    function BuyAsset(uint256 _assetId) external whenNotPaused payable {
        require(msg.sender != address(0));
        require(msg.sender != address(this));
        CollectibleSale memory _sale = tokenIdToSale[_assetId];
        require(_isOnSale(_sale));
        
        //address seller = _sale.seller;

        _buy(_assetId, msg.sender, msg.value);
    }

    function GetAssetTypeAverageSalePrice(uint256 _assetType) public view returns (uint256) {
        uint256 sum = 0;
        for (uint256 i = 0; i < avgSalesToCount; i++) {
            sum += assetTypeSalePrices[_assetType].sales[i];
        }
        return sum / 5;
    }

    /// @dev Override unpause so it requires all external contract addresses
    ///  to be set before contract can be unpaused. Also, we can't have
    ///  newContractAddress set either, because then the contract was upgraded.
    /// @notice This is public rather than external so we can call super.unpause
    ///  without using an expensive CALL.
    function unpause() public anyOperator whenPaused {
        // Actually unpause the contract.
        super.unpause();
    }

    /// @dev Remove all Ether from the contract, which is the owner's cuts
    ///  as well as any Ether sent directly to the contract address.
    ///  Always transfers to the NFT (ERC721) contract, but can be called either by
    ///  the owner or the NFT (ERC721) contract.
    function withdrawBalance() public onlyBanker {
        // We are using this boolean method to make sure that even if one fails it will still work
        bankManager.transfer(address(this).balance);
    }

    /// @dev Returns sales info for an CSLCollectibles (ERC721) on sale.
    /// @param _assetId - ID of the token on sale
    function getSale(uint256 _assetId) external view returns (address seller, uint256 startingPrice, uint256 endingPrice, uint256 duration, uint256 startedAt, bool isActive, address buyer, uint256 tokenId) {
        CollectibleSale memory sale = tokenIdToSale[_assetId];
        require(_isOnSale(sale));
        return (
            sale.seller,
            sale.startingPrice,
            sale.endingPrice,
            sale.duration,
            sale.startedAt,
            sale.isActive,
            sale.buyer,
            sale.tokenId
        );
    }


    /** Internal Functions */

    function _createSale(uint256 _tokenId, uint256 _startingPrice, uint256 _endingPrice, uint64 _duration, address _seller) internal {
        var ccNFT = CCNFTFactory(NFTAddress);

        require(ccNFT.isAssetIdOwnerOrApproved(this, _tokenId) == true);
        
        CollectibleSale memory onSale = tokenIdToSale[_tokenId];
        require(onSale.isActive == false);

        // Sanity check that no inputs overflow how many bits we've allocated
        // to store them in the sale struct.
        require(_startingPrice == uint256(uint128(_startingPrice)));
        require(_endingPrice == uint256(uint128(_endingPrice)));
        require(_duration == uint256(uint64(_duration)));

        //Transfer ownership if needed
        if(ccNFT.ownerOf(_tokenId) != address(this)) {
            
            require(ccNFT.isApprovedForAll(msg.sender, this) == true);

            ccNFT.safeTransferFrom(ccNFT.ownerOf(_tokenId), this, _tokenId);
        }

        CollectibleSale memory sale = CollectibleSale(
            _seller,
            uint128(_startingPrice),
            uint128(_endingPrice),
            uint64(_duration),
            uint64(now),
            true,
            address(0),
            uint256(_tokenId)
        );
        _addSale(_tokenId, sale);
    }

    /// @dev Adds an sale to the list of open sales. Also fires the
    ///  SaleCreated event.
    function _addSale(uint256 _assetId, CollectibleSale _sale) internal {
        // Require that all sales have a duration of
        // at least one minute.
        require(_sale.duration >= 1 minutes);
        
        tokenIdToSale[_assetId] = _sale;

        var ccNFT = CCNFTFactory(NFTAddress);
        uint256 assetType = ccNFT.getAssetIdItemType(_assetId);
        assetTypeSalesTokenId[assetType].push(_assetId);

        SaleCreated(
            uint256(_assetId),
            uint256(_sale.startingPrice),
            uint256(_sale.endingPrice),
            uint256(_sale.duration),
            uint64(_sale.startedAt)
        );
    }

    /// @dev Returns current price of a Collectible (ERC721) on sale. Broken into two
    ///  functions (this one, that computes the duration from the sale
    ///  structure, and the other that does the price computation) so we
    ///  can easily test that the price computation works correctly.
    function _currentPrice(CollectibleSale memory _sale) internal view returns (uint256) {
        uint256 secondsPassed = 0;

        // A bit of insurance against negative values (or wraparound).
        // Probably not necessary (since Ethereum guarnatees that the
        // now variable doesn't ever go backwards).
        if (now > _sale.startedAt) {
            secondsPassed = now - _sale.startedAt;
        }

        return _computeCurrentPrice(
            _sale.startingPrice,
            _sale.endingPrice,
            _sale.duration,
            secondsPassed
        );
    }

    /// @dev Computes the current price of an sale. Factored out
    ///  from _currentPrice so we can run extensive unit tests.
    ///  When testing, make this function public and turn on
    ///  `Current price computation` test suite.
    function _computeCurrentPrice(uint256 _startingPrice, uint256 _endingPrice, uint256 _duration, uint256 _secondsPassed) internal pure returns (uint256) {
        // NOTE: We don't use SafeMath (or similar) in this function because
        //  all of our public functions carefully cap the maximum values for
        //  time (at 64-bits) and currency (at 128-bits). _duration is
        //  also known to be non-zero (see the require() statement in
        //  _addSale())
        if (_secondsPassed >= _duration) {
            // We've reached the end of the dynamic pricing portion
            // of the sale, just return the end price.
            return _endingPrice;
        } else {
            // Starting price can be higher than ending price (and often is!), so
            // this delta can be negative.
            int256 totalPriceChange = int256(_endingPrice) - int256(_startingPrice);

            // This multiplication can't overflow, _secondsPassed will easily fit within
            // 64-bits, and totalPriceChange will easily fit within 128-bits, their product
            // will always fit within 256-bits.
            int256 currentPriceChange = totalPriceChange * int256(_secondsPassed) / int256(_duration);

            // currentPriceChange can be negative, but if so, will have a magnitude
            // less that _startingPrice. Thus, this result will always end up positive.
            int256 currentPrice = int256(_startingPrice) + currentPriceChange;

            return uint256(currentPrice);
        }
    }

    function _buy(uint256 _assetId, address _buyer, uint256 _price) internal {

        CollectibleSale storage _sale = tokenIdToSale[_assetId];

        // Check that the bid is greater than or equal to the current buyOut price
        uint256 currentPrice = _currentPrice(_sale);

        require(_price >= currentPrice);
        _sale.buyer = _buyer;
        _sale.isActive = false;

        _removeSale(_assetId);

        uint256 bidExcess = _price - currentPrice;
        _buyer.transfer(bidExcess);

        var ccNFT = CCNFTFactory(NFTAddress);
        uint256 assetType = ccNFT.getAssetIdItemType(_assetId);
        _updateSaleAvgHistory(assetType, _price);
        ccNFT.safeTransferFrom(this, _buyer, _assetId);

        emit SaleWinner(_buyer, _assetId, _price);
    }

    function _cancelSale (uint256 _assetId) internal {
        CollectibleSale storage _sale = tokenIdToSale[_assetId];

        require(_sale.isActive == true);

        address sellerAddress = _sale.seller;

        _removeSale(_assetId);

        var ccNFT = CCNFTFactory(NFTAddress);

        ccNFT.safeTransferFrom(this, sellerAddress, _assetId);

        emit SaleCancelled(sellerAddress, _assetId);
    }
    
    /// @dev Returns true if the FT (ERC721) is on sale.
    function _isOnSale(CollectibleSale memory _sale) internal view returns (bool) {
        return (_sale.startedAt > 0 && _sale.isActive);
    }

    function _updateSaleAvgHistory(uint256 _assetType, uint256 _price) internal {
        assetTypeSaleCount[_assetType] += 1;
        assetTypeSalePrices[_assetType].sales[assetTypeSaleCount[_assetType] % avgSalesToCount] = _price;
    }

    /// @dev Removes an sale from the list of open sales.
    /// @param _assetId - ID of the token on sale
    function _removeSale(uint256 _assetId) internal {
        delete tokenIdToSale[_assetId];

        var ccNFT = CCNFTFactory(NFTAddress);
        uint256 assetType = ccNFT.getAssetIdItemType(_assetId);

        bool hasFound = false;
        for (uint i = 0; i < assetTypeSalesTokenId[assetType].length; i++) {
            if ( assetTypeSalesTokenId[assetType][i] == _assetId) {
                hasFound = true;
            }
            if(hasFound == true) {
                if(i+1 < assetTypeSalesTokenId[assetType].length)
                    assetTypeSalesTokenId[assetType][i] = assetTypeSalesTokenId[assetType][i+1];
                else 
                    delete assetTypeSalesTokenId[assetType][i];
            }
        }
        assetTypeSalesTokenId[assetType].length--;
    }


    // Vending

    function setVendingAttachedState (uint256 _collectibleType, uint256 _state) external onlyManager {
        vendingAttachedState = _state;
    }

    /// @dev Function toggle vending for collectible
    function setVendingAmount (uint256 _collectibleType, uint256 _vendingQty) external onlyManager {
        vendingAmountType[_collectibleType] = _vendingQty;
    }

    /// @dev Function sets the starting price / reset price
    function setVendingStartPrice (uint256 _collectibleType, uint256 _startingPrice) external onlyManager {
        vendingPrice[_collectibleType] = _startingPrice;
    }

    /// @dev Sets Step Value
    function setVendingStepValues(uint256 _collectibleType, uint256 _stepAmount, uint256 _stepQty) external onlyManager {
        vendingStepUpQty[_collectibleType] = _stepQty;
        vendingStepUpAmount[_collectibleType] = _stepAmount;
    }

    /// @dev Create Vending Helper
    function createVendingItem(uint256 _collectibleType, uint256 _vendingQty, uint256 _startingPrice, uint256 _stepAmount, uint256 _stepQty) external onlyManager {
        vendingAmountType[_collectibleType] = _vendingQty;
        vendingPrice[_collectibleType] = _startingPrice;
        vendingStepUpQty[_collectibleType] = _stepQty;
        vendingStepUpAmount[_collectibleType] = _stepAmount;
    }

    /// @dev This helps in creating a collectible and then 
    /// transfer it _toAddress
    function vendingCreateCollectible(uint256 _collectibleType, address _toAddress) payable external whenNotPaused {
        
        //Only if Vending is Allowed for this Asset
        require((vendingAmountType[_collectibleType] - vendingTypeSold[_collectibleType]) > 0);

        require(msg.value >= vendingPrice[_collectibleType]);

        require(msg.sender != address(0));
        require(msg.sender != address(this));
        
        require(_toAddress != address(0));
        require(_toAddress != address(this));

        var ccNFT = CCNFTFactory(NFTAddress);

        ccNFT.spawnAsset(_toAddress, _collectibleType, startingIndex, vendingAttachedState);

        startingIndex += 1;

        vendingTypeSold[_collectibleType] += 1;

        uint256 excessBid = msg.value - vendingPrice[_collectibleType];

        if(vendingTypeSold[_collectibleType] % vendingStepUpQty[_collectibleType] == 0) {
            vendingPrice[_collectibleType] += vendingStepUpAmount[_collectibleType];
        }

        if(excessBid > 0) {
            msg.sender.transfer(excessBid);
        }
 
    }

    function getVendingAmountLeft (uint256 _collectibleType) view public returns (uint256) {
        return (vendingAmountType[_collectibleType] - vendingTypeSold[_collectibleType]);
    }

    function getVendingAmountSold (uint256 _collectibleType) view public returns (uint256) {
        return (vendingTypeSold[_collectibleType]);
    }

    function getVendingPrice (uint256 _collectibleType) view public returns (uint256) {
        return (vendingPrice[_collectibleType]);
    }

    function getVendingStepPrice (uint256 _collectibleType) view public returns (uint256) {
        return (vendingStepUpAmount[_collectibleType]);
    }

    function getVendingStepQty (uint256 _collectibleType) view public returns (uint256) {
        return (vendingStepUpQty[_collectibleType]);
    }

    function getVendingInfo (uint256 _collectibleType) view public returns (uint256 amountRemaining, uint256 sold, uint256 price, uint256 stepPrice, uint256 stepQty) {
        amountRemaining = (vendingAmountType[_collectibleType] - vendingTypeSold[_collectibleType]);
        sold = vendingTypeSold[_collectibleType];
        price = vendingPrice[_collectibleType];
        stepPrice = vendingStepUpAmount[_collectibleType];
        stepQty = vendingStepUpQty[_collectibleType];
    }

}
function _isOnSale(CollectibleSale memory _sale) internal view returns (bool) {
    return (_sale.startedAt > 0 && _sale.isActive);
}

/// @dev Returns true if the FT (ERC721) is on sale.