//Copyright(C) 2017 GarWare
//
//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//any later version.
//
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with this program.If not, see <http://www.gnu.org/licenses/>.
//
//          |---------------GarWare Members---------------|
//              Rúben Garcia
//              Rúben Fonseca
//              Ricardo Martinho
//
//          |---------------Contacts---------------|
//              Rúben Garcia: rubenribeirogarcia@gmail.com
//              Rúben Fonseca:
//              Ricardo Martinho:
//
//          |---------------Description---------------|
//              This is a simple class that generates an random password, depending on what arguments you give to the function.
//

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;


/**
 * 
 * Generate Password Class
 *
 */
public class Security 
{
	//Static Variables
	private static String SPECIAL_CHARS = "*$-+?_&=!%{}/";
	
	static class CharGroupSelectorConstant
	{
		public static final int LOWER = 0;
		public static final int UPPER = 1;
		public static final int NUMERIC = 2;
		public static final int SPECIAL = 3;
		public static final int NULL = 4;
		
	}
	
	//Char Group Enumerate
	private enum CharGroupSelector
	{
		//Lower Case Chars
		LOWER,
		//Upper Case Chars
		UPPER,
		//NUmeric Chars
		NUMERIC,
		//Special Chars
		SPECIAL,
		//Null
		NULL
	}
	
    public static String GeneratePassword(int minLength, int maxLength, int minLowerCaseChars, int minUpperCaseChars, int minNumericChars, int minSpecialChars) throws GeneratePasswordException
    {
        return StartGeneratingPassword(minLength, maxLength, minLowerCaseChars, minUpperCaseChars, minNumericChars, minSpecialChars);
    }
    
    public static String GeneratePassword(int length, int minLowerCaseChars, int minUpperCaseChars, int minNumericChars, int minSpecialChars) throws GeneratePasswordException
    {
        return StartGeneratingPassword(length, length, minLowerCaseChars, minUpperCaseChars, minNumericChars, minSpecialChars);
    }
       
    private static String StartGeneratingPassword(int minLength, int maxLength, int minLowerCaseChars, int minUpperCaseChars, int minNumericChars, int minSpecialChars) throws GeneratePasswordException 
    {

        //If the length is less than the sum of all minimums, than it shouldn't generate a password
        if (minLength < minLowerCaseChars + minUpperCaseChars + minNumericChars + minSpecialChars)
            throw new GeneratePasswordException("The sum of all minimums requirements are greater than minimum password length.");

        //If the maxLength is less than minLength, than it shouldn't generate a password
        if (maxLength < minLength)
            throw new GeneratePasswordException("The maxLength must be greater or equal that minLength");

        //Incialize Random with seed
        Random random = inicializeRandom();

        //Inicialize an group of chars
        Map<CharGroupSelector, char[]> GroupOfChars = inicializeChars();

        //Inicialize an reference of minimums of each group of chars
        Map<CharGroupSelector, Integer> MinimumOfEachCharGroup = inicializeMinimumCharGroupDic(GroupOfChars, minLowerCaseChars, minUpperCaseChars, minNumericChars, minSpecialChars);

        String generatedPassword = "";

        //Inicialize a password char array with a random size depending on the Min and Max Password Length
        char[] password = new char[random.nextInt(Math.abs(maxLength - minLength + 1)) + minLength];
        
        //Incialize an list with all empty spaces in a password
        List<Integer> passwordEmptySpaces = listEmptyPositions(password.length);

        //Picks all minimuns first, to fill the password with those groups
        List<CharGroupSelector> minimumReqList = getMinimumGroupOfChars(MinimumOfEachCharGroup);

        int listMinimumsCount = minimumReqList.size();

        // Puts caracters in password until all requirements are meets 
        for (int i = 0; i < listMinimumsCount; i++)
        {
            int selectorIndex = random.nextInt(minimumReqList.size());

            
            //IMPORTANTE ref password PASSOU PARA password SO, NAO SEI SE CASA PROBLEMAS OU NAO
            password = fillPasswordRandomlyBasedOnSelector(GroupOfChars, MinimumOfEachCharGroup, random, password, passwordEmptySpaces, minimumReqList.get(selectorIndex));

            minimumReqList.remove(minimumReqList.get(selectorIndex));
        }

        // If still exists space to more caracters new a choosen randomly
        if (passwordEmptySpaces.size() > 0)
        {
            int listEmptyCount = passwordEmptySpaces.size();

            int nextCharIndex;

            int nextGroupCharsIndex;

            int nextPositionInPassword;

            for (int i = 0; i < listEmptyCount; i++)
            {
                nextGroupCharsIndex = random.nextInt( GroupOfChars.size());

                nextCharIndex = random.nextInt(GroupOfChars.get(getEnumByValue(nextGroupCharsIndex)).length);

                nextPositionInPassword = passwordEmptySpaces.get(random.nextInt(passwordEmptySpaces.size()));

                password[nextPositionInPassword] = GroupOfChars.get(getEnumByValue(nextGroupCharsIndex))[nextCharIndex];

                passwordEmptySpaces.remove((Integer)nextPositionInPassword);
            }
        }

        generatedPassword = new String(password);

        return generatedPassword;
    }
    
    //Region Initializers
    
    private static Random inicializeRandom()
    {
        // Use a 4-byte array to fill it with random bytes and convert it then
        // to an integer value.
        byte[] randomBytes = new byte[4];

        // Generate 4 random bytes.
        SecureRandom secureRandom = new SecureRandom();
        randomBytes = secureRandom.generateSeed(randomBytes.length);

        // Convert 4 bytes into a 32-bit integer value.
        int seed = java.nio.ByteBuffer.wrap(randomBytes).getInt();

        // Now, this is real randomization.
        Random random = new Random(seed);

        return random;
    }
    
    private static Map<CharGroupSelector, char[]> inicializeChars()
    {
    	Map<CharGroupSelector, char[]> charsGroup = new HashMap<CharGroupSelector, char[]>();
    	
    	charsGroup.put(CharGroupSelector.LOWER, GetCharRange('a', 'z', ""));
    	charsGroup.put(CharGroupSelector.UPPER, GetCharRange('A','Z', ""));
    	charsGroup.put(CharGroupSelector.NUMERIC, GetCharRange('0', '9', ""));
    	charsGroup.put(CharGroupSelector.SPECIAL, SPECIAL_CHARS.toCharArray());
    	
        return charsGroup;
    }
    
    private static char[] GetCharRange(char minimum, char maximum, String exclusiveChars)
    {
        char[] result = new char[maximum-minimum + 1];
        for (char value = minimum; value <= maximum; value++)
        {
            result[value - minimum] = value;
        }
        if (exclusiveChars != null || exclusiveChars != "")
        {
        	char[] exclusiveCharsArray = new char[result.length - exclusiveChars.length()];
        	int i = 0;
        	for(char value : result)
        	{
        		boolean toPutInside = true;
        		for(char exclusiveChar : exclusiveChars.toCharArray())
        		{
        			if(value == exclusiveChar)
        				toPutInside = false;      			
        		}
        		if(toPutInside)
        			exclusiveCharsArray[i] = value;
        	}
        }
        return result;
    }
	
    private static Map<CharGroupSelector, Integer> inicializeMinimumCharGroupDic(Map<CharGroupSelector, char[]> charsGroup, int minLowerCaseChars, int minUpperCaseChars, int minNumericChars, int minSpecialChars)
    {
    	Map<CharGroupSelector, Integer> minimumCharGroupDic = new HashMap<CharGroupSelector, Integer>();

    	Set<Entry<CharGroupSelector, char[]>> set = charsGroup.entrySet();
    	Iterator<Entry<CharGroupSelector, char[]>> it = set.iterator();
		Entry<CharGroupSelector, char[]> entry;
    	while(it.hasNext()){
    		
    		entry = it.next();   		
            switch (entry.getKey())
            {
                case LOWER:
                    minimumCharGroupDic.put(CharGroupSelector.LOWER, minLowerCaseChars);
                    break;
                case UPPER:
                    minimumCharGroupDic.put(CharGroupSelector.UPPER, minUpperCaseChars);
                    break;
                case NUMERIC:
                    minimumCharGroupDic.put(CharGroupSelector.NUMERIC, minNumericChars);
                    break;
                case SPECIAL:
                    minimumCharGroupDic.put(CharGroupSelector.SPECIAL, minSpecialChars);
                    break;
                default:
                	/*Shit might happen ?*/
                	break;
            }
        }
        return minimumCharGroupDic;
    }
    
    //End Region Initializers
    
    //Region Helpers
    
    private static CharGroupSelector getEnumByValue(int value)
    {
        CharGroupSelector sel = CharGroupSelector.NULL;
        switch (value)
        {
            case CharGroupSelectorConstant.LOWER:
                sel = CharGroupSelector.LOWER;
                break;
            case CharGroupSelectorConstant.UPPER:
                sel = CharGroupSelector.UPPER;
                break;
            case CharGroupSelectorConstant.NUMERIC:
                sel = CharGroupSelector.NUMERIC;
                break;
            case CharGroupSelectorConstant.SPECIAL:
                sel = CharGroupSelector.SPECIAL;
                break;
        }
        return sel;
    }
    
    private static List<Integer> listEmptyPositions(int passwordLength)
    {
        List<Integer> emptySpaces = new ArrayList<Integer>(passwordLength*50);
        for (int i = 0; i < passwordLength; i++)
        {
            emptySpaces.add(i);
        }

        return emptySpaces;
    }
    
    private static List<CharGroupSelector> getMinimumGroupOfChars(Map<CharGroupSelector, Integer> minimumOfEachCharGroup)
    {
    	List<CharGroupSelector> list = new ArrayList<CharGroupSelector>();

    	Set<Entry<CharGroupSelector, Integer>> set = minimumOfEachCharGroup.entrySet();
    	Iterator<Entry<CharGroupSelector, Integer>> it = set.iterator();

    	while(it.hasNext())
    	{
    		Entry<CharGroupSelector, Integer> entry = it.next();
    		if(entry.getValue() > 0)
    			list.add(entry.getKey());
    	}
        return list;
    }
    
    private static char[] fillPasswordRandomlyBasedOnSelector(Map<CharGroupSelector, char[]> GroupOfChars, Map<CharGroupSelector, Integer> MinimumOfEachCharGroup, Random random, char[] password, List<Integer> passwordEmptySpaces, CharGroupSelector selector)
    {
        int nextCharIndex;

        int nextPositionInPassword;

        for (int i = 0; i < MinimumOfEachCharGroup.get(selector); i++)
        {
            nextCharIndex = random.nextInt(GroupOfChars.get(selector).length);

            nextPositionInPassword = passwordEmptySpaces.get(random.nextInt(passwordEmptySpaces.size()));

            password[nextPositionInPassword] = GroupOfChars.get(selector)[nextCharIndex];
            
            passwordEmptySpaces.remove((Integer)nextPositionInPassword);
        }

        return password;
    }
    
    //End Region Helpers
    
    //Region Generate Password Exception
    
    public static class GeneratePasswordException extends Exception
    {
        /**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		
		public String message;

        public GeneratePasswordException(String message)
        {
            this.message = message;
        }
    }
    
    //End Region Generate Password Exception
}
