contract PriIPoolTemplate is IPool, InitializableOwnable, Rootable {
    using SafeMath for uint256;

    mapping(address => uint256) private _orders;
    mapping(address => Participation) private _participations;
    uint256 private _startTime;
    uint256 private _endTime;
    uint256 private _claimTime;
    uint256 private _tokenOutSupply;
    uint256 private _tokenOutReserved;
    uint256 private _totalAmountOfTokenIn;
    uint256 private _upperLimitOfTokenIn;
    bool private _isOver = false;
    string private _proName;
    string private _proLink;

    IPoolFactory public _FACTORY_;

    event PrivateOffering(address indexed participant, uint256 amountOfTokenIn, uint256 amountOfTokenOut);
    event PrivateOfferingClaimed(address indexed participant, uint256 amountOfTokenOut);

    uint256 private _tokenOutPrice;// TOKENOUT/TOKENIN * FACTOR
    uint256 public constant _FACTOR_ = 10000;

    mapping(address => uint8) private _whiteList;

    IERC20  public _TOKEN_IN_;
    IERC20  public _TOKEN_OUT_;

    bool private _withdrawn = false;
    uint8 private unlocked = 1;
    modifier lock() {
        require(unlocked == 1, 'LOCKED');
        unlocked = 0;
        _;
        unlocked = 1;
    }
    bool private _activated = false;
    modifier activated() {
        require(_activated, "NOT_ACTIVATED");
        _;
    }
    modifier whenNotActivated() {
        require(!_activated, "ACTIVATED");
        _;
    }

    uint256 private _feeRate = 10;//10% as default
    address private _feeTo;

    address private _vault;
    bool private _homePageShow;
    bool public initialized;

    function init(
        address __factory,
        address __root,
        address __creator,
        string calldata __proName,
        uint256 __startTime,
        uint256 __endTime,
        address __tokenIn,
        address __tokenOut,
        uint256 __tokenOutSupply,
        uint256 __upperLimitOfTokenIn,
        uint256 __tokenOutPrice
    ) external {
        require(!initialized, "INITIALIZED");
        initOwner(__creator);
        initRoot(__root);
        _feeTo = __root;
        _vault = __creator;
        _FACTORY_ = IPoolFactory(__factory);
        _proName = __proName;
        _startTime = __startTime;
        _endTime = __endTime;
        _TOKEN_IN_ = IERC20(__tokenIn);
        _TOKEN_OUT_ = IERC20(__tokenOut);
        _tokenOutSupply = __tokenOutSupply;
        _upperLimitOfTokenIn = __upperLimitOfTokenIn;
        _tokenOutPrice = __tokenOutPrice;
        _totalAmountOfTokenIn = 0;
        initialized = true;
        unlocked = 1;
    }

    function setTokenOutPrice(uint256 newPrice) external whenNotActivated onlyOwner {
        _tokenOutPrice = newPrice;
    }

    function inWhiteList(address account) external view returns (bool) {
        return _whiteList[account] == 1;
    }

    function addToWhiteList(address account) external onlyOwner {
        _whiteList[account] = 1;
    }

    function addBatchToWhiteList(address[] calldata accounts) external onlyOwner {
        for (uint i = 0; i < accounts.length; i++) {
            _whiteList[accounts[i]] = 1;
        }
    }

    function removeFromWhiteList(address account) external onlyOwner {
        _whiteList[account] = 0;
    }

    function tokenOutSupply() external view returns (uint256) {
        return _tokenOutSupply;
    }

    function tokenOutReserved() external view returns (uint256) {
        return _tokenOutReserved;
    }

    function totalAmountOfTokenIn() external view returns (uint256) {
        return _totalAmountOfTokenIn;
    }

    function setTokenOutSupply(uint256 newSupply) external whenNotActivated onlyOwner {
        _tokenOutSupply = newSupply;
    }

    function setUpperLimitOfTokenIn(uint256 newVal) external whenNotActivated onlyOwner {
        _upperLimitOfTokenIn = newVal;
    }

    function upperLimitOfTokenIn() external view returns (uint256) {
        return _upperLimitOfTokenIn;
    }

    //IPool
    function getParticipation(address participant) external override view returns (
        uint256 timeStamp,
        uint256 amountOfTokenIn,
        uint256 availableAmountOfTokenIn,
        uint256 amountOfTokenOut,
        bool claimed
    ){
        Participation memory p = _participations[participant];
        return (p.timeStamp, p.amountOfTokenIn, p.availableAmountOfTokenIn, p.amountOfTokenOut, p.claimed);
    }

    function proName() external override view returns (string memory){
        return _proName;
    }

    function proLink() external override view returns (string memory){
        return _proLink;
    }

    function homePageShow() external override view returns (bool){
        return _homePageShow;
    }

    function poolType() external view override returns (uint8){
        return 1;
    }

    function startTime() external view override returns (uint256){
        return _startTime;
    }

    function endTime() external override view returns (uint256){
        return _endTime;
    }

    function tokenOutPrice() external override view returns (uint256){
        return _tokenOutPrice;
    }

    function nameOfTokenOut() external override view returns (string memory) {
        return _TOKEN_OUT_.name();
    }

    function symbolOfTokenOut() external override view returns (string memory) {
        return _TOKEN_OUT_.symbol();
    }

    function symbolOfTokenIn() external override view returns (string memory) {
        return _TOKEN_IN_.symbol();
    }

    function decimalsOfTokenOut() external override view returns (uint8) {
        return _TOKEN_OUT_.decimals();
    }

    function isOver() public override view returns (bool) {
        return block.timestamp > _endTime || _isOver;
    }

    function canClaim(address participant) public override view returns (bool){
        return isOver() && _orders[participant] > 0 && block.timestamp >= _claimTime;
    }

    function state() external override view returns (uint8){
        if (_activated) {
            if (block.timestamp < _startTime) {//NOT STARTED
                return 1;
            }
            else if (!_isOver && block.timestamp >= _startTime && block.timestamp <= _endTime) {//IN PROGRESS
                return 2;
            }
            else {//IS OVER
                return 3;
            }
        }
        else {//NOT ACTIVATED
            return 0;
        }
    }

    function setStartTime(uint256 time) onlyOwner external override whenNotActivated returns (bool){
        require(time > block.timestamp.add(300), "INVALID_START_TIME");
        _startTime = time;
        return true;
    }

    function setEndTime(uint256 time) onlyOwner external override whenNotActivated returns (bool){
        require(time > block.timestamp.add(600), "INVALID_END_TIME");
        _endTime = time;
        return true;
    }

    function setVault(address newVault) onlyOwner external override whenNotActivated returns (bool){
        require(Address.isNotZero(newVault), 'ZERO_ADDRESS_NOT_ALLOWED');
        _vault = newVault;
        return true;
    }

    function activate(bool bActivated, bool bHomePageShow, string calldata sProlink, uint256 feeRate) onlyRoot external override returns (bool){
        require(_startTime > block.timestamp.add(300), "INVALID_START_TIME");
        require(_endTime >= _startTime.add(300), "INVALID_END_TIME");
        require(Address.isContract(address(_TOKEN_IN_)), "INVALID_TOKEN_IN");
        require(Address.isContract(address(_TOKEN_OUT_)), "INVALID_TOKEN_OUT");
        require(feeRate <= 100, "INVALID_FEE_RATE");

        if (_claimTime < _endTime) {
            _claimTime = _endTime;
        }

        _activated = bActivated;
        _homePageShow = bHomePageShow;
        _proLink = sProlink;
        _feeRate = feeRate;

        return true;
    }

    function swap(uint256 amountOfTokenIn) activated lock external override returns (bool)  {
        require(_whiteList[msg.sender] == 1, "NOT_IN_WHITE_LIST");
        require(block.timestamp >= _startTime, 'NOT_STARTED');
        require(!isOver(), 'PRIVATE_OFFERING_IS_OVER');
        require(_orders[msg.sender] == 0, 'ENROLLED_ALREADY');
        require(amountOfTokenIn <= _upperLimitOfTokenIn, 'EXCEEDS_THE_UPPER_LIMIT');
        require(amountOfTokenIn > 0, "INVALID_AMOUNT");
        require(_tokenOutReserved < _tokenOutSupply, 'INSUFFICIENT_TOKEN_OUT');

        uint256 tokenOutDeci = uint256(10) ** _TOKEN_OUT_.decimals();
        uint256 tokenInDeci = uint256(10) ** _TOKEN_IN_.decimals();

        uint256 amountOfTokenOut = amountOfTokenIn.mul(tokenOutDeci).mul(_tokenOutPrice).div(_FACTOR_).div(tokenInDeci);

        if (_tokenOutReserved.add(amountOfTokenOut) >= _tokenOutSupply) {
            amountOfTokenOut = _tokenOutSupply.sub(_tokenOutReserved);
            amountOfTokenIn = amountOfTokenOut.mul(_FACTOR_).mul(tokenInDeci).div(tokenOutDeci).div(_tokenOutPrice);
            _isOver = true;
        }

        _transferFrom(address(_TOKEN_IN_), msg.sender, address(this), amountOfTokenIn);
        _orders[msg.sender] = amountOfTokenOut;
        _tokenOutReserved = _tokenOutReserved.add(amountOfTokenOut);
        _totalAmountOfTokenIn = _totalAmountOfTokenIn.add(amountOfTokenIn);
        emit PrivateOffering(msg.sender, amountOfTokenIn, amountOfTokenOut);

        _FACTORY_.enroll(msg.sender, address(this));
        Participation storage p = _participations[msg.sender];
        p.timeStamp = block.timestamp;
        p.amountOfTokenIn = amountOfTokenIn;
        p.availableAmountOfTokenIn = amountOfTokenIn;
        p.amountOfTokenOut = amountOfTokenOut;

        return true;
    }

    function claim() lock external override returns (bool){
        require(canClaim(msg.sender), 'FORBIDDEN');

        uint256 reserved = _orders[msg.sender];
        _transfer(address(_TOKEN_OUT_), msg.sender, reserved);
        _orders[msg.sender] = 0;
        emit PrivateOfferingClaimed(msg.sender, reserved);

        Participation storage p = _participations[msg.sender];
        p.claimed = true;

        return true;
    }

    function withdraw() onlyRoot external override returns (bool){
        require(isOver(), 'IS_NOT_OVER');
        require(!_withdrawn, 'WITHDRAWN');

        uint256 fee = _totalAmountOfTokenIn.mul(_feeRate).div(100);
        if (fee > 0) {
            _transfer(address(_TOKEN_IN_), _feeTo, fee);
        }

        uint256 reserve = _totalAmountOfTokenIn.sub(fee);
        if (reserve > 0) {
            _transfer(address(_TOKEN_IN_), _vault, reserve);
        }

        _withdrawn = true;
        return true;
    }

    function claimTime() external override view returns (uint256){
        return _claimTime;
    }

    function setClaimTime(uint256 time) onlyRoot external override returns (bool){
        require(time > _endTime, 'FORBIDDEN');
        _claimTime = time;
        return true;
    }

    function getFeeRate() external override view returns (uint256){
        return _feeRate;
    }

    function getFeeTo() external override view returns (address){
        return _feeTo;
    }

    function setFeeTo(address account) onlyRoot override external returns (bool){
        _feeTo = account;
    }

    //Only can call  this function 7 days later when PRI OFFERING was over, in case of emergency
    function emergencyWithdraw(address token, address recipient, uint256 amount) onlyRoot external {
        require(block.timestamp > _claimTime.add(7 days), 'FORBIDDEN');

        bytes4 methodId = bytes4(keccak256(bytes('transfer(address,uint256)')));

        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(methodId, recipient, amount));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FAILED');
    }

    function _transfer(address token, address recipient, uint256 amount) private {
        bytes4 methodId = bytes4(keccak256(bytes('transfer(address,uint256)')));

        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(methodId, recipient, amount));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FAILED');
    }

    function _transferFrom(address token, address sender, address recipient, uint256 amount) private {
        bytes4 methodId = bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));

        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(methodId, sender, recipient, amount));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FROM_FAILED');
    }
}
function emergencyWithdraw(address token, address recipient, uint256 amount) onlyRoot external {
    require(block.timestamp > _claimTime.add(7 days), 'FORBIDDEN');

    bytes4 methodId = bytes4(keccak256(bytes('transfer(address,uint256)')));

    (bool success, bytes memory data) = token.call(abi.encodeWithSelector(methodId, recipient, amount));
    require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FAILED');
}

//Only can call  this function 7 days later when PRI OFFERING was over, in case of emergency