contract SecurityController is ISecurityController, Ownable {
    ISecurityLedger public ledger;
    ISecurityToken public token;
    ISecuritySale public sale;
    IInvestorList public investorList;
    ITransferAuthorizations public transferAuthorizations;
    IAffiliateList public affiliateList;

    uint public lockoutPeriod = 10 * 60 * 60; // length in seconds of the lockout period

    // restrict who can grant transfer authorizations
    mapping(address => bool) public transferAuthPermission;

    constructor() public {
    }

    function setTransferAuthorized(address from, address to, uint expiry) public {
        // Must be called from address in the transferAuthPermission mapping
        require(transferAuthPermission[msg.sender]);

        // don't allow 'from' to be zero
        require(from != 0);

        // verify expiry is in future, but not more than 30 days
        if(expiry > 0) {
            require(expiry > block.timestamp);
            require(expiry <= (block.timestamp + 30 days));
        }

        transferAuthorizations.set(from, to, expiry);
    }

    // functions below this line are onlyOwner

    function setLockoutPeriod(uint _lockoutPeriod) public onlyOwner {
        lockoutPeriod = _lockoutPeriod;
    }

    function setToken(address _token) public onlyOwner {
        token = ISecurityToken(_token);
    }

    function setLedger(address _ledger) public onlyOwner {
        ledger = ISecurityLedger(_ledger);
    }

    function setSale(address _sale) public onlyOwner {
        sale = ISecuritySale(_sale);
    }

    function setInvestorList(address _investorList) public onlyOwner {
        investorList = IInvestorList(_investorList);
    }

    function setTransferAuthorizations(address _transferAuthorizations) public onlyOwner {
        transferAuthorizations = ITransferAuthorizations(_transferAuthorizations);
    }

    function setAffiliateList(address _affiliateList) public onlyOwner {
        affiliateList = IAffiliateList(_affiliateList);
    }

    function setTransferAuthPermission(address agent, bool hasPermission) public onlyOwner {
        require(agent != address(0));
        transferAuthPermission[agent] = hasPermission;
    }

    modifier onlyToken() {
        require(msg.sender == address(token));
        _;
    }

    modifier onlyLedger() {
        require(msg.sender == address(ledger));
        _;
    }

    // public functions

    function totalSupply() public view returns (uint) {
        return ledger.totalSupply();
    }

    function balanceOf(address _a) public view returns (uint) {
        return ledger.balanceOf(_a);
    }

    function allowance(address _owner, address _spender) public view returns (uint) {
        return ledger.allowance(_owner, _spender);
    }

    function isTransferAuthorized(address _from, address _to) public view returns (bool) {
        // A `from` address could have both an allowance for the `to` address
        // and a global allowance (to the zero address). We pick the maximum
        // of the two.

        uint expiry = transferAuthorizations.get(_from, _to);
        uint globalExpiry = transferAuthorizations.get(_from, 0);
        if(globalExpiry > expiry) {
            expiry = globalExpiry;
        }

        return expiry > block.timestamp;
    }

    /**
     * @dev Determines whether the given transfer is possible. Returns multiple
     *      boolean flags specifying how the transfer must occur.
     *      This is kept public to provide for testing and subclasses overriding behavior.
     * @param _from Address the tokens are being transferred from
     * @param _to Address the tokens are being transferred to
     * @param _value Number of tokens that would be transferred
     * @param lockoutTime A point in time, specified in epoch time, that specifies
     *                    the lockout period (typically 1 year before now).
     * @return canTransfer Whether the transfer can occur at all.
     * @return useLockoutTime Whether the lockoutTime should be used to determine which tokens to transfer.
     * @return newTokensAreRestricted Whether the transferred tokens should be marked as restricted.
     * @return preservePurchaseDate Whether the purchase date on the tokens should be preserved, or reset to 'now'.
     */
    function checkTransfer(address _from, address _to, uint _value, uint lockoutTime)
        public
        returns (bool canTransfer, bool useLockoutTime, bool newTokensAreRestricted, bool preservePurchaseDate) {

        // DEFAULT BEHAVIOR:
        //
        // If there exists a Transfer Agent authorization, allow transfer regardless
        //
        // All transfers from an affiliate must be authorized by Transfer Agent
        //   - tokens become restricted
        //
        // From Reg S to Reg S: allowable, regardless of holding period
        //
        // otherwise must meet holding period

        // presently this isn't used, so always setting to false to avoid warning
        preservePurchaseDate = false;

        bool transferIsAuthorized = isTransferAuthorized(_from, _to);

        bool fromIsAffiliate = affiliateList.inListAsOf(_from, block.timestamp);
        bool toIsAffiliate = affiliateList.inListAsOf(_to, block.timestamp);

        if(transferIsAuthorized) {
            canTransfer = true;
            if(fromIsAffiliate || toIsAffiliate) {
                newTokensAreRestricted = true;
            }
            // useLockoutTime will remain false
            // preservePurchaseDate will remain false
        }
        else if(!fromIsAffiliate) {
            // see if both are Reg S
            if(investorList.hasRole(_from, investorList.ROLE_REGS())
                && investorList.hasRole(_to, investorList.ROLE_REGS())) {
                canTransfer = true;
                // newTokensAreRestricted will remain false
                // useLockoutTime will remain false
                // preservePurchaseDate will remain false
            }
            else {
                if(ledger.transferDryRun(_from, _to, _value, lockoutTime) == _value) {
                    canTransfer = true;
                    useLockoutTime = true;
                    // newTokensAreRestricted will remain false
                    // preservePurchaseDate will remain false
                }
            }
        }
    }

    // functions below this line are onlyLedger

    // let the ledger send transfer events (the most obvious case
    // is when we mint directly to the ledger and need the Transfer()
    // events to appear in the token)
    function ledgerTransfer(address from, address to, uint val) public onlyLedger {
        token.controllerTransfer(from, to, val);
    }

    // functions below this line are onlyToken

    function transfer(address _from, address _to, uint _value) public onlyToken returns (bool success) {
        //TODO: this could be configurable
        uint lockoutTime = block.timestamp - lockoutPeriod;
        bool canTransfer;
        bool useLockoutTime;
        bool newTokensAreRestricted;
        bool preservePurchaseDate;
        (canTransfer, useLockoutTime, newTokensAreRestricted, preservePurchaseDate)
            = checkTransfer(_from, _to, _value, lockoutTime);

        if(!canTransfer) {
            return false;
        }

        uint overrideLockoutTime = lockoutTime;
        if(!useLockoutTime) {
            overrideLockoutTime = 0;
        }

        return ledger.transfer(_from, _to, _value, overrideLockoutTime, newTokensAreRestricted, preservePurchaseDate);
    }

    function transferFrom(address _spender, address _from, address _to, uint _value) public onlyToken returns (bool success) {
        //TODO: this could be configurable
        uint lockoutTime = block.timestamp - lockoutPeriod;
        bool canTransfer;
        bool useLockoutTime;
        bool newTokensAreRestricted;
        bool preservePurchaseDate;
        (canTransfer, useLockoutTime, newTokensAreRestricted, preservePurchaseDate)
            = checkTransfer(_from, _to, _value, lockoutTime);

        if(!canTransfer) {
            return false;
        }

        uint overrideLockoutTime = lockoutTime;
        if(!useLockoutTime) {
            overrideLockoutTime = 0;
        }

        return ledger.transferFrom(_spender, _from, _to, _value, overrideLockoutTime, newTokensAreRestricted, preservePurchaseDate);
    }

    function approve(address _owner, address _spender, uint _value) public onlyToken returns (bool success) {
        return ledger.approve(_owner, _spender, _value);
    }

    function increaseApproval (address _owner, address _spender, uint _addedValue) public onlyToken returns (bool success) {
        return ledger.increaseApproval(_owner, _spender, _addedValue);
    }

    function decreaseApproval (address _owner, address _spender, uint _subtractedValue) public onlyToken returns (bool success) {
        return ledger.decreaseApproval(_owner, _spender, _subtractedValue);
    }

    function burn(address _owner, uint _amount) public onlyToken {
        ledger.burn(_owner, _amount);
    }
}
function checkTransfer(address _from, address _to, uint _value, uint lockoutTime)
        public
        returns (bool canTransfer, bool useLockoutTime, bool newTokensAreRestricted, bool preservePurchaseDate) {

        // DEFAULT BEHAVIOR:
        //
        // If there exists a Transfer Agent authorization, allow transfer regardless
        //
        // All transfers from an affiliate must be authorized by Transfer Agent
        //   - tokens become restricted
        //
        // From Reg S to Reg S: allowable, regardless of holding period
        //
        // otherwise must meet holding period

        // presently this isn't used, so always setting to false to avoid warning
        preservePurchaseDate = false;

        bool transferIsAuthorized = isTransferAuthorized(_from, _to);

        bool fromIsAffiliate = affiliateList.inListAsOf(_from, block.timestamp);
        bool toIsAffiliate = affiliateList.inListAsOf(_to, block.timestamp);

        if(transferIsAuthorized) {
            canTransfer = true;
            if(fromIsAffiliate || toIsAffiliate) {
                newTokensAreRestricted = true;
            }
            // useLockoutTime will remain false
            // preservePurchaseDate will remain false
        }
        else if(!fromIsAffiliate) {
            // see if both are Reg S
            if(investorList.hasRole(_from, investorList.ROLE_REGS())
                && investorList.hasRole(_to, investorList.ROLE_REGS())) {
                canTransfer = true;
                // newTokensAreRestricted will remain false
                // useLockoutTime will remain false
                // preservePurchaseDate will remain false
            }
            else {
                if(ledger.transferDryRun(_from, _to, _value, lockoutTime) == _value) {
                    canTransfer = true;
                    useLockoutTime = true;
                    // newTokensAreRestricted will remain false
                    // preservePurchaseDate will remain false
                }
            }
        }
    }
/**
 * @dev Determines whether the given transfer is possible. Returns multiple
 *      boolean flags specifying how the transfer must occur.
 *      This is kept public to provide for testing and subclasses overriding behavior.
 * @param _from Address the tokens are being transferred from
 * @param _to Address the tokens are being transferred to
 * @param _value Number of tokens that would be transferred
 * @param lockoutTime A point in time, specified in epoch time, that specifies
 *                    the lockout period (typically 1 year before now).
 * @return canTransfer Whether the transfer can occur at all.
 * @return useLockoutTime Whether the lockoutTime should be used to determine which tokens to transfer.
 * @return newTokensAreRestricted Whether the transferred tokens should be marked as restricted.
 * @return preservePurchaseDate Whether the purchase date on the tokens should be preserved, or reset to 'now'.
 */