contract GunFactory is Ownable {
    using strings for *;
    
    uint8 public constant PREMIUM_CATEGORY = 1;
    uint8 public constant MIDGRADE_CATEGORY = 2;
    uint8 public constant REGULAR_CATEGORY = 3;
    uint256 public constant ONE_MONTH = 2628000;
    
    uint256 public mintedGuns = 0;
    address preOrderAddress;
    GunToken token;
    
    mapping(uint8 => uint256) internal gunsMintedByCategory;
    mapping(uint8 => uint256) internal totalGunsMintedByCategory;
    
    mapping(uint8 => uint256) internal firstMonthLimit;
    mapping(uint8 => uint256) internal secondMonthLimit;
    mapping(uint8 => uint256) internal thirdMonthLimit;
    
    uint256 internal startTime;
    mapping(uint8 => uint256) internal currentMonthEnd;
    uint256 internal monthOneEnd;
    uint256 internal monthTwoEnd;

    modifier onlyPreOrder {
        require(msg.sender == preOrderAddress, "Not authorized");
        _;
    }

    modifier isInitialized {
        require(preOrderAddress != address(0), "No linked preorder");
        require(address(token) != address(0), "No linked token");
        _;
    }
    
    constructor() public {
        firstMonthLimit[PREMIUM_CATEGORY] = 5000;
        firstMonthLimit[MIDGRADE_CATEGORY] = 20000;
        firstMonthLimit[REGULAR_CATEGORY] = 30000;
        
        secondMonthLimit[PREMIUM_CATEGORY] = 2500;
        secondMonthLimit[MIDGRADE_CATEGORY] = 10000;
        secondMonthLimit[REGULAR_CATEGORY] = 15000;
        
        thirdMonthLimit[PREMIUM_CATEGORY] = 600;
        thirdMonthLimit[MIDGRADE_CATEGORY] = 3000;
        thirdMonthLimit[REGULAR_CATEGORY] = 6000;
        
        startTime = block.timestamp;
        monthOneEnd = startTime + ONE_MONTH;
        monthTwoEnd = startTime + ONE_MONTH + ONE_MONTH;
        
        currentMonthEnd[PREMIUM_CATEGORY] = monthOneEnd;
        currentMonthEnd[MIDGRADE_CATEGORY] = monthOneEnd;
        currentMonthEnd[REGULAR_CATEGORY] = monthOneEnd;
    }

    function uintToString(uint v) internal pure returns (string memory) {
        if (v == 0) {
            return "0";
        }
        uint j = v;
        uint len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint k = len - 1;
        while (v != 0) {
            bstr[k--] = byte(uint8(48 + v % 10));
            v /= 10;
        }
        
        return string(bstr);
    }

    function mintFor(address newOwner, uint16 size, uint8 category) public onlyPreOrder isInitialized returns (uint256) {
        GunPreOrder preOrder = GunPreOrder(preOrderAddress);
        require(preOrder.categoryExists(category), "Invalid category");
        
        require(!hasReachedLimit(category), "The monthly limit has been reached");
        
        token.claimAllocation(newOwner, size, category);
        
        mintedGuns++;
        
        gunsMintedByCategory[category] = gunsMintedByCategory[category] + 1;
        totalGunsMintedByCategory[category] = totalGunsMintedByCategory[category] + 1;
    }
    
    function hasReachedLimit(uint8 category) internal returns (bool) {
        uint256 currentTime = block.timestamp;
        uint256 limit = currentLimit(category);
        
        uint256 monthEnd = currentMonthEnd[category];
        
        //If the current block time is greater than or equal to the end of the month
        if (currentTime >= monthEnd) {
            //It's a new month, reset all limits
            //gunsMintedByCategory[PREMIUM_CATEGORY] = 0;
            //gunsMintedByCategory[MIDGRADE_CATEGORY] = 0;
            //gunsMintedByCategory[REGULAR_CATEGORY] = 0;
            gunsMintedByCategory[category] = 0;
            
            //Set next month end to be equal one month in advance
            //do this while the current time is greater than the next month end
            while (currentTime >= monthEnd) {
                monthEnd = monthEnd + ONE_MONTH;
            }
            
            //Finally, update the limit
            limit = currentLimit(category);
            currentMonthEnd[category] = monthEnd;
        }
        
        //Check if the limit has been reached
        return gunsMintedByCategory[category] >= limit;
    }
    
    function reachedLimit(uint8 category) public view returns (bool) {
        uint256 limit = currentLimit(category);
        
        return gunsMintedByCategory[category] >= limit;
    }
    
    function currentLimit(uint8 category) public view returns (uint256) {
        uint256 currentTime = block.timestamp;
        uint256 limit;
        if (currentTime < monthOneEnd) {
            limit = firstMonthLimit[category];
        } else if (currentTime < monthTwoEnd) {
            limit = secondMonthLimit[category];
        } else {
            limit = thirdMonthLimit[category];
        }
        
        return limit;
    }
    
    function setCategoryLimit(uint8 category, uint256 firstLimit, uint256 secondLimit, uint256 thirdLimit) public onlyOwner {
        require(firstMonthLimit[category] == 0);
        require(secondMonthLimit[category] == 0);
        require(thirdMonthLimit[category] == 0);
        
        firstMonthLimit[category] = firstLimit;
        secondMonthLimit[category] = secondLimit;
        thirdMonthLimit[category] = thirdLimit;
    }
    
    /**
    Attach the preOrder that will be receiving tokens being marked for sale by the
    sellCar function
    */
    function attachPreOrder(address dst) public onlyOwner {
        require(preOrderAddress == address(0));
        require(dst != address(0));

        //Enforce that address is indeed a preorder
        GunPreOrder preOrder = GunPreOrder(dst);

        preOrderAddress = address(preOrder);
    }

    /**
    Attach the token being used for things
    */
    function attachToken(address dst) public onlyOwner {
        require(address(token) == address(0));
        require(dst != address(0));

        //Enforce that address is indeed a preorder
        GunToken ct = GunToken(dst);

        token = ct;
    }
}
function attachToken(address dst) public onlyOwner {
    require(address(token) == address(0));
    require(dst != address(0));

    //Enforce that address is indeed a preorder
    GunToken ct = GunToken(dst);

    token = ct;
}

/**
 Attach the token being used for things
 */