contract Time is ERC20TokenImplementation {

    /**
     * @dev TimeZones are candidates possible to be yield farmed
     */

    function _max_int() pure private returns (uint256) {
        return 2 ** 256 - 1;
    }

    function _seconds_in_day() pure private 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) onlyOwner private 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);
            uint keyListIndex = _timeZoneKeys.length - 1;
            timeZoneToUpsert.index = keyListIndex + 1;
        }
        return true;
    }

    function _removeTimeZone(string calldata id) onlyOwner private returns (bool) {
        TimeZone storage timeZoneToRemove = _timeZoneMap[id];
        // entry not exist
        require(timeZoneToRemove.index != 0);
        // invalid index value
        require(timeZoneToRemove.index <= _timeZoneKeys.length);

        // Move an last element of array into the vacated key slot.
        uint keyListIndex = timeZoneToRemove.index - 1;
        uint keyListLastIndex = _timeZoneKeys.length - 1;
        _timeZoneMap[_timeZoneKeys[keyListLastIndex]].index = keyListIndex + 1;
        _timeZoneKeys[keyListIndex] = _timeZoneKeys[keyListLastIndex];
        _timeZoneKeys.pop();
        delete _timeZoneMap[id];
        return true;
    }

    function _timeZonesSize() view private returns (uint) {
        return uint(_timeZoneKeys.length);
    }

    function _timeZoneExists(string calldata id) view private returns (bool) {
        return _timeZoneMap[id].index > 0;
    }

    function _timeZoneById(string calldata id) view private returns (TimeZone memory) {
        return _timeZoneMap[id];
    }

    function _timeZoneByIndex(uint index) view private returns (TimeZone memory) {
        require(index >= 0);
        require(index < _timeZoneKeys.length);
        return _timeZoneMap[_timeZoneKeys[index]];
    }

    function _timeZonesKeys() view private returns (string[] memory) {
        return _timeZoneKeys;
    }

    /**
     * @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);
            uint 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);
        // invalid index value
        require(votingProportionToRemove.index <= _votingProportionKeys.length);

        // Move an last element of array into the vacated key slot.
        uint keyListIndex = votingProportionToRemove.index - 1;
        uint keyListLastIndex = _votingProportionKeys.length - 1;
        _votingProportionMap[_votingProportionKeys[keyListLastIndex]].index = keyListIndex + 1;
        _votingProportionKeys[keyListIndex] = _votingProportionKeys[keyListLastIndex];
        _votingProportionKeys.pop();
        delete _votingProportionMap[key];
        return true;
    }

    function _votingProportionSize() view private returns (uint) {
        return uint(_votingProportionKeys.length);
    }

    function _votingProportionExists(address key) view private returns (bool) {
        return _votingProportionMap[key].index > 0;
    }

    function _votingProportionByKey(address key) view private returns (VotingProportion memory) {
        return _votingProportionMap[key];
    }

    function _votingProportionByIndex(uint index) view private returns (VotingProportion memory) {
        require(index >= 0);
        require(index < _votingProportionKeys.length);
        return _votingProportionMap[_votingProportionKeys[index]];
    }


    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 = _max_int();
        _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 {
        _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) onlyOwner external 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) onlyOwner external returns (bool) {
        require(_removeTimeZone(id), "Failed to remove time zone!");
        emit TimeZoneRemoved(id);
        return true;
    }

    function timeZone(string calldata id) view external returns (TimeZone memory) {
        return _timeZoneById(id);
    }

    function timeZoneIds() view external 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;
        for (uint256 i = 0; i < zoneIds.length; i++) {
            allKeyExists = allKeyExists && _timeZoneExists(zoneIds[i]);
            proportionSum += proportions[i];
        }
        require(allKeyExists, "Input contains wrong cadidate 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) view external returns (VotingProportion memory) {
        return _votingProportionByKey(account);
    }

    function votingResult() view external 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;
            for (uint256 j = 0; j < zoneIds.length; j++) {
                uint256 zoneIndex = _timeZoneMap[zoneIds[j]].index - 1;
                uint256 accountVotingProportion = _votingProportionMap[account].proportions[j];
                votingResultToReturn[zoneIndex] += SafeMath.div(SafeMath.mul(accountBalance, accountVotingProportion), 1 ether);
            }
        }
        return votingResultToReturn;
    }

    function cleanVotingProportion() onlyOwner external returns (bool) {
        for (uint256 i = 0; i < _votingProportionSize(); i++) {
            delete _votingProportionMap[_votingProportionKeys[i]];
        }
        delete _votingProportionKeys;
        emit VotingProportionCleaned();
        return true;
    }

    function cleanTimeZones() onlyOwner external returns (bool) {
        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) onlyOwner external returns (bool) {
        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() view external returns (address) {
        return _baseCurrency;
    }

    /**
     * @dev changes base currency contract, need to provide rate which is newBaseCurrency/currentBaseCurrency
     */
    function setBaseCurrency(address newBaseCurrency, uint256 rate) onlyOwner external returns (bool) {
        address previousBaseCurrency = _baseCurrency;
        _baseCurrency = newBaseCurrency;
        for (uint256 timeStamp = _minTimeStamp; timeStamp <= _maxTimeStamp; timeStamp = timeStamp + _seconds_in_day()) {
            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) {
        return SafeMath.div(SafeMath.mul(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() view public returns (uint256) {
        return SafeMath.div(SafeMath.mul(_baseLiquidityAmount(), 1 ether), currentPrice());
    }

    /**
     * @dev the actual live price - this price is used in maxTimeSellAmount and sellForBase
     */
    function currentPrice() view public returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).computedPrice;
    }

    /**
     * @dev start price of the current timeframe
     */
    function currentStartPrice() view public 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() view public returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).priceTo;
    }

    /**
     * @dev start timestamp of the current timeframe
     */
    function currentStartTimeStamp() view public returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).timeStampFrom;
    }

    /**
     * @dev end timestamp of the current timeframe
     */
    function currentEndTimeStamp() view public returns (uint256) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp).timeStampTo;
    }

    /**
     * @dev full priceinfo object of the current timeframe
     */
    function currentPriceInfo() view public returns (PriceInfo memory) {
        uint256 currentTimeStamp = block.timestamp;
        return _computePriceInfo(currentTimeStamp);
    }

    /**
     * @dev price of a given historical time
     */
    function historicalPrice(uint256 timeStamp) view public returns (uint256) {
        return _computePriceInfo(timeStamp).computedPrice;
    }

    /**
     * @dev full priceinfo object of a given historical time
     */
    function historicalPriceInfo(uint256 timeStamp) view public returns (PriceInfo memory) {
        return _computePriceInfo(timeStamp);
    }

    function _computePriceInfo(uint256 timeStamp) view internal 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 = SafeMath.mod(timeStamp, _seconds_in_day());

        uint256 timeStampFrom = SafeMath.sub(timeStamp, secondInDay);
        uint256 timeStampTo = SafeMath.add(timeStampFrom, _seconds_in_day());
        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 = SafeMath.add(SafeMath.div(SafeMath.mul(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) {
        uint256 baseAmount = SafeMath.div(timeAmount * currentEndPrice(), 1 ether);
        IERC20(_baseCurrency).transferFrom(msg.sender, _liquidity, baseAmount);
        _mint(msg.sender, timeAmount);
        emit TimeBought(timeAmount, baseAmount, msg.sender);
        return true;
    }

    /**
     * @dev sell time coin with base currency current price
     */
    function sellForBase(uint256 timeAmount) external returns (bool) {
        require(maxTimeSellAmount() > timeAmount, "Time amount to sell should be lower than maxTimeSellAmount()");
        uint256 baseAmount = SafeMath.div(timeAmount * currentPrice(), 1 ether);
        IERC20(_baseCurrency).transferFrom(_liquidity, msg.sender, baseAmount);
        _burn(msg.sender, timeAmount);
        emit TimeSold(timeAmount, baseAmount, msg.sender);
        return true;
    }

    /**
     * @dev reimburse expense
     */
    function reimburse() onlyOwner external returns (bool) {
        require(_baseLiquidityAmount() > _expense, "Not enough base liquidity amount!");
        IERC20(_baseCurrency).transferFrom(_liquidity, msg.sender, _expense);
        emit Reimbursed(_expense);
        _expense = 0;
        return true;
    }

    function expenseAmount() view external returns (uint256) {
        return _expense;
    }

    /**
     * @dev run proof of lapse process using interest gained and amount of released coin (excluding owned by time itself)
     */
    function proofOfLapse(uint256 capitalGain, uint256 releasedCoins, uint256 newTimeStamp, uint256 expense) onlyOwner external returns (bool) {
        require(SafeMath.mod(newTimeStamp, _seconds_in_day()) == 0, "TimeStamp should be divisible by 86400!");
        uint256 previousPrice = historicalPrice(SafeMath.sub(newTimeStamp, _seconds_in_day()));
        uint256 newPrice = SafeMath.add(previousPrice, SafeMath.div(SafeMath.mul(SafeMath.sub(SafeMath.sub(capitalGain, expense), _expense), 1 ether), releasedCoins));
        return proofOfLapse(newPrice, newTimeStamp, expense);
    }

    /**
     * @dev run proof of lapse process using new end price
     */
    function proofOfLapse(uint256 newPrice, uint256 newTimeStamp, uint256 expense) onlyOwner public returns (bool) {
        require(SafeMath.mod(newTimeStamp, _seconds_in_day()) == 0, "TimeStamp should be divisible by 86400!");
        _expense = SafeMath.add(_expense, expense);
        _timeStampToPrice[newTimeStamp] = newPrice;
        if (newTimeStamp < _minTimeStamp) {
            _minTimeStamp = newTimeStamp;
        }
        if (_maxTimeStamp < newTimeStamp) {
            _maxTimeStamp = newTimeStamp;
        }
        emit LapseProved(newPrice, newTimeStamp, expense);
        return true;
    }
}
function proofOfLapse(uint256 capitalGain, uint256 releasedCoins, uint256 newTimeStamp, uint256 expense) onlyOwner external returns (bool) {
    require(SafeMath.mod(newTimeStamp, _seconds_in_day()) == 0, "TimeStamp should be divisible by 86400!");
    uint256 previousPrice = historicalPrice(SafeMath.sub(newTimeStamp, _seconds_in_day()));
    uint256 newPrice = SafeMath.add(previousPrice, SafeMath.div(SafeMath.mul(SafeMath.sub(SafeMath.sub(capitalGain, expense), _expense), 1 ether), releasedCoins));
    return proofOfLapse(newPrice, newTimeStamp, expense);
}

/**
 * @dev run proof of lapse process using interest gained and amount of released coin (excluding owned by time itself)
 */