// Contract: Time
// Target function: function sellForBase(uint256 timeAmount) external returns (bool)
(spender != address(0), "ERC20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev Destroys `amount` tokens from `account`.`amount` is then deducted
     * from the caller's allowance.
     *
     * See {_burn} and {_approve}.
     */
    function _burnFrom(address account, uint256 amount) internal {
        _burn(account, amount);
        _approve(account, _msgSender(), _allowances[account][_msgSender()].sub(amount, "ERC20: burn amount exceeds allowance"));
    }
}

contract Time is ERC20TokenImplementation {

    /**
     * @dev TimeZones are candidates possible to be yield farmed
     */

    function _maxInt() private pure returns (uint256) {
        return 2 ** 256 - 1;
    }

    function _secondsInDay() private pure returns (uint256) {
        return 86400;
    }

    struct TimeZone {
        uint256 index;
        string id;
        string description;
        address contractAddress;
    }

    mapping(string => TimeZone) private _timeZoneMap;
    string[] private _timeZoneKeys;


    function _upsertTimeZone(string calldata id, string calldata description, address contractAddress) private onlyOwner returns (bool) {
        TimeZone storage timeZoneToUpsert = _timeZoneMap[id];
        timeZoneToUpsert.id = id;
        timeZoneToUpsert.description = description;
        timeZoneToUpsert.contractAddress = contractAddress;
        if (timeZoneToUpsert.index > 0) {// entry exists
            // do nothing
            return true;
        } else {// new entry
            _timeZoneKeys.push(id);
            uint256 keyListIndex = _timeZoneKeys.length - 1;
            timeZoneToUpsert.index = keyListIndex + 1;
        }
        return true;
    }

    function _removeTimeZone(string calldata id) private onlyOwner returns (bool) {
        TimeZone storage timeZoneToRemove = _timeZoneMap[id];
        // entry not exist
        require(timeZoneToRemove.index != 0, "Provided TimeZone does not exist!");
        // invalid index value
        require(timeZoneToRemove.index <= _timeZoneKeys.length, "Provided TimeZone index is invalid!");

        // Move an last element of array into the vacated key slot.
        uint256 keyListIndex = timeZoneToRemove.index - 1;
        uint256 keyListLastIndex = _timeZoneKeys.length - 1;
        _timeZoneMap[_timeZoneKeys[keyListLastIndex]].index = keyListIndex + 1;
        _timeZoneKeys[keyListIndex] = _timeZoneKeys[keyListLastIndex];
        _timeZoneKeys.pop();
        delete _timeZoneMap[id];
        return true;
    }

    function _timeZonesSize() private view returns (uint256) {
        return uint256(_timeZoneKeys.length);
    }

    function _timeZoneExists(string calldata id) private view returns (bool) {
        return _timeZoneMap[id].index > 0;
    }

    function _timeZoneById(string calldata id) private view returns (TimeZone memory) {
        return _timeZoneMap[id];
    }

    /**
     * @dev Voting proportion of an account for which time zone to farm from
     */
    struct VotingProportion {
        uint256 index;
        string[] zoneIds;
        uint256[] proportions;
    }

    mapping(address => VotingProportion) private _votingProportionMap;
    address[] private _votingProportionKeys;

    function _upsertVotingProportion(address key, string[] memory zoneIds, uint256[] memory proportions) private returns (bool) {
        VotingProportion storage votingProportionToUpsert = _votingProportionMap[key];
        votingProportionToUpsert.zoneIds = zoneIds;
        votingProportionToUpsert.proportions = proportions;
        if (votingProportionToUpsert.index > 0) {// entry exists
            // do nothing
            return true;
        } else {// new entry
            _votingProportionKeys.push(key);
            uint256 keyListIndex = _votingProportionKeys.length - 1;
            votingProportionToUpsert.index = keyListIndex + 1;
        }
        return true;
    }

    function _removeVotingProportion(address key) private returns (bool) {
        VotingProportion storage votingProportionToRemove = _votingProportionMap[key];
        // entry not exist
        require(votingProportionToRemove.index != 0, "Provided voting proportion does not exist!");
        // invalid index value
        require(votingProportionToRemove.index <= _votingProportionKeys.length, "Provided voting proportion index is invalid!");

        // Move an last element of array into the vacated key slot.
        uint256 keyListIndex = votingProportionToRemove.index - 1;
        uint256 keyListLastIndex = _votingProportionKeys.length - 1;
        _votingProportionMap[_votingProportionKeys[keyListLastIndex]].index = keyListIndex + 1;
        _votingProportionKeys[keyListIndex] = _votingProportionKeys[keyListLastIndex];
        _votingProportionKeys.pop();
        delete _votingProportionMap[key];
        return true;
    }

    function _votingProportionSize() private view returns (uint256) {
        return uint256(_votingProportionKeys.length);
    }

    function _votingProportionByKey(address key) private view returns (VotingProportion memory) {
        return _votingProportionMap[key];
    }


    struct PriceInfo {
        uint256 timeStampFrom;
        uint256 timeStampTo;
        uint256 priceFrom;
        uint256 priceTo;
        uint256 computedPrice;
    }

    /**
     * @dev Admin private variables
     */
    address private _liquidity;
    address private _baseCurrency;
    uint256 private _expense;

    /**
     * @dev Time pricing related private variables
     */
    uint256 private _minTimeStamp;
    uint256 private _maxTimeStamp;
    mapping(uint256 => uint256) private _timeStampToPrice;

    // Voting Events
    event TimeZoneUpdated(string id, string description, address contractAddress);
    event TimeZoneRemoved(string id);
    event Voted(address votingAccount, string[] zoneIds, uint256[] proportions);
    event Unvoted(address votingAccount);
    event TimeZonesCleaned();
    event VotingProportionCleaned();

    // Admin Events
    event LiquidityChanged(address previousLiquidity, address newLiquidity);
    event BaseCurrencyChanged(address previousBaseCurrency, address newBaseCurrency, uint256 rate);

    // Time Events
    event TimeBought(uint256 timeAmount, uint256 baseCurrencyAmount, address buyer);
    event TimeSold(uint256 timeAmount, uint256 baseCurrencyAmount, address seller);
    event LapseProved(uint256 newPrice, uint256 newTimeStamp, uint256 expense);
    event Reimbursed(uint256 expense);

    constructor() {
        _timeZoneKeys = new string[](0);
        _votingProportionKeys = new address[](0);
        _maxTimeStamp = 0;
        _minTimeStamp = _maxInt();
        _expense = 0;
    }

    /**
     * @dev sets initials supply and the owner
     */
    function initialize(string memory name, string memory symbol, uint8 decimals, uint256 amount, bool mintable, address owner, address liquidityIn, address baseCurrencyIn) external initializer {
        require(owner != address(0), "Address(owner) cannot be zero!");
        require(liquidityIn != address(0), "Address(liquidityIn) cannot be zero!");
        require(baseCurrencyIn != address(0), "Address(baseCurrencyIn) cannot be zero!");
        _owner = owner;
        _name = name;
        _symbol = symbol;
        _decimals = decimals;
        _mintable = mintable;
        _liquidity = liquidityIn;
        _baseCurrency = baseCurrencyIn;
        _mint(owner, amount);
    }

    function addOrUpdateTimeZone(string calldata id, string calldata description, address contractAddress) external onlyOwner returns (bool) {
        require(_upsertTimeZone(id, description, contractAddress), "Failed to upsert new time zone!");
        emit TimeZoneUpdated(id, description, contractAddress);
        return true;
    }

    function removeTimeZone(string calldata id) external onlyOwner returns (bool) {
        require(_removeTimeZone(id), "Failed to remove time zone!");
        emit TimeZoneRemoved(id);
        return true;
    }

    function timeZone(string calldata id) external view returns (TimeZone memory) {
        return _timeZoneById(id);
    }

    function timeZoneIds() external view returns (string[] memory) {
        return _timeZoneKeys;
    }

    function vote(string[] calldata zoneIds, uint256[] calldata proportions) external returns (bool) {
        require(zoneIds.length == proportions.length, "Voting zone length and proportion length does not match!");
        bool allKeyExists = true;
        uint256 proportionSum = 0;
        uint256 zoneIdsLength = zoneIds.length;
        for (uint256 i = 0; i < zoneIdsLength; i++) {
            allKeyExists = allKeyExists && _timeZoneExists(zoneIds[i]);
            proportionSum += proportions[i];
        }
        require(allKeyExists, "Input contains wrong candidate id(s)!");
        require(proportionSum == 1 ether, "Voting proportion must add up to 1 ether");
        require(_upsertVotingProportion(msg.sender, zoneIds, proportions), "Failed to update voting proportions!");
        emit Voted(msg.sender, zoneIds, proportions);
        return true;
    }

    function unvote() external returns (bool) {
        require(_removeVotingProportion(msg.sender), "Failed to remove voting proportion!");
        emit Unvoted(msg.sender);
        return true;
    }

    function votingProportion(address account) external view returns (VotingProportion memory) {
        return _votingProportionByKey(account);
    }

    function votingResult() external view returns (uint256[] memory) {
        uint256[] memory votingResultToReturn = new uint256[](_timeZonesSize());
        for (uint256 i = 0; i < _votingProportionSize(); i++) {
            address account = _votingProportionKeys[i];
            uint256 accountBalance = _balances[account];
            string[] storage zoneIds = _votingProportionMap[account].zoneIds;
            uint256 zoneIdsLength = zoneIds.length;
            for (uint256 j = 0; j < zoneIdsLength; j++) {
                uint256 zoneIndex = _timeZoneMap[zoneIds[j]].index - 1;
                uint256 accountVotingProportion = _votingProportionMap[account].proportions[j];
                votingResultToReturn[zoneIndex] += (accountBalance * accountVotingProportion) / 1 ether;
            }
        }
        return votingResultToReturn;
    }

    function cleanVotingProportion() external onlyOwner returns (bool) {
        uint256 votingProportionSize = _votingProportionSize();
        for (uint256 i = 0; i < votingProportionSize; i++) {
            delete _votingProportionMap[_votingProportionKeys[i]];
        }
        delete _votingProportionKeys;
        emit VotingProportionCleaned();
        return true;
    }

    function cleanTimeZones() external onlyOwner returns (bool) {
        uint256 timeZonesSize = _timeZonesSize();
        for (uint256 i = 0; i < timeZonesSize; i++) {
            delete _timeZoneMap[_timeZoneKeys[i]];
        }
        delete _timeZoneKeys;
        emit TimeZonesCleaned();
        return true;
    }

    /**
     * @dev changes the liquidity account
     */
    function setLiquidity(address liquidity) external onlyOwner returns (bool) {
        require(liquidity != address(0), "Address(liquidity) cannot be zero!");
        address previousLiquidity = _liquidity;
        _liquidity = liquidity;
        emit LiquidityChanged(previousLiquidity, _liquidity);
        return true;
    }

    /**
     * @dev main currency that time is backed - most likely one of the fiat base stablecoin
     */
    function baseCurrency() external view returns (address) {
        return _baseCurrency;
    }

    /**
     * @dev changes base currency contract, need to provide rate which is newBaseCurrency/currentBaseCurrency
     */
    function setBaseCurrency(address newBaseCurrency, uint256 rate) external onlyOwner returns (bool) {
        require(newBaseCurrency != address(0), "Address(newBaseCurrency) cannot be zero!");
        address previousBaseCurrency = _baseCurrency;
        _baseCurrency = newBaseCurrency;
        for (uint256 timeStamp = _minTimeStamp; timeStamp <= _maxTimeStamp; timeStamp = timeStamp + _secondsInDay()) {
            if (_timeStampToPrice[timeStamp] != 0) {
                _timeStampToPrice[timeStamp] = _applyRate(_timeStampToPrice[timeStamp], rate);
            }
        }
        emit BaseCurrencyChanged(previousBaseCurrency, _baseCurrency, rate);
        return true;
    }

    function _applyRate(uint256 value, uint256 rate) internal pure returns (uint256) {
        require(value * rate > 1 ether, "Provided rate is too small!");
        return (value * rate) / 1 ether;
    }

    function _baseLiquidityAmount() internal view returns (uint256) {
        return IERC20(_baseCurrency).balanceOf(address(_liquidity));
    }

    /**
     * @dev maximum amount time can be sold back at the moment
     * if this value is small, time service will refill liquidity with base currency
     */
    function maxTimeSellAmount() public view returns (uint256) {
        return (_baseLiquidityAmount() * 1 ether) / currentPrice();
    }

    /**
     * @dev the actual live price - this price is used in maxTimeSellAmount and sellForBase
     */
    function currentPrice() public view returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).computedPrice;
    }

    /**
     * @dev start price of the current timeframe
     */
    function currentStartPrice() public view returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).priceFrom;
    }

    /**
     * @dev end price of the current timeframe
     * when buying time, it can be only bought with this price
     */
    function currentEndPrice() public view returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).priceTo;
    }

    /**
     * @dev start timestamp of the current timeframe
     */
    function currentStartTimeStamp() public view returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).timeStampFrom;
    }

    /**
     * @dev end timestamp of the current timeframe
     */
    function currentEndTimeStamp() public view returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).timeStampTo;
    }

    /**
     * @dev full priceinfo object of the current timeframe
     */
    function currentPriceInfo() public view returns (PriceInfo memory) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp);
    }

    /**
     * @dev price of a given historical time
     */
    function historicalPrice(uint256 timeStamp) public view returns (uint256) {
        return _computePriceInfo(timeStamp).computedPrice;
    }

    /**
     * @dev full priceinfo object of a given historical time
     */
    function historicalPriceInfo(uint256 timeStamp) public view returns (PriceInfo memory) {
        return _computePriceInfo(timeStamp);
    }

    function _computePriceInfo(uint256 timeStamp) internal view returns (PriceInfo memory) {
        if (timeStamp <= _minTimeStamp) {
            uint256 minPrice = _timeStampToPrice[_minTimeStamp];
            return PriceInfo(_minTimeStamp, _minTimeStamp, minPrice, minPrice, minPrice);
        }
        if (_maxTimeStamp <= timeStamp) {
            uint256 maxPrice = _timeStampToPrice[_maxTimeStamp];
            return PriceInfo(_maxTimeStamp, _maxTimeStamp, maxPrice, maxPrice, maxPrice);
        }

        uint256 secondInDay = timeStamp % _secondsInDay();

        uint256 timeStampFrom = timeStamp - secondInDay;
        uint256 timeStampTo = timeStampFrom + _secondsInDay();
        uint256 priceFrom = _timeStampToPrice[timeStampFrom];
        uint256 priceTo = _timeStampToPrice[timeStampTo];

        if (secondInDay == 0) {
            return PriceInfo(timeStampFrom, timeStampTo, priceFrom, priceTo, priceFrom);
        }
        if (priceTo <= priceFrom) {
            return PriceInfo(timeStampFrom, timeStampTo, priceFrom, priceTo, priceTo);
        }

        uint256 priceRange = priceTo - priceFrom;
        uint256 timeRange = timeStampTo - timeStampFrom;
        uint256 timePassed = timeStamp - timeStampFrom;
        uint256 price = ((priceRange * timePassed) / timeRange) + priceFrom;
        return PriceInfo(timeStampFrom, timeStampTo, priceFrom, priceTo, price);
    }

    /**
     * @dev buy time coin with base currency end price
     */
    function buyForBase(uint256 timeAmount) external returns (bool) {
        require(timeAmount * currentEndPrice() > 1 ether, "Provided timeAmount is too small!");
        uint256 baseAmount = (timeAmount * currentEndPrice()) / 1 ether;
        IERC20(_baseCurrency).transferFrom(msg.sender, _liquidity, baseAmount);
        _mint(msg.sender, timeAmount);
        emit TimeBought(timeAmount, baseAmount, msg.sender);
        return true;
    }