package railo.runtime.op.validators;

import railo.commons.lang.StringUtil;
import railo.runtime.exp.ExpressionException;
import railo.runtime.op.Caster;

/**
 * logic to determine if a credit card number is valid. no GUI, just
 * calculation.
 *
 */
public final class ValidateCreditCard {

    /**
     * enum for Amex
     */
    static final int AMEX = 1;

    /**
     * true if debugging output wanted
     */
    static final boolean DEBUGGING = true;

    /**
     * enum for Diner's club
     */
    static final int DINERS = 2; // includes Carte Blanche

    /**
     * enum for Discover Card
     */
    static final int DISCOVER = 3;

    /**
     * enum for Enroute
     */
    static final int ENROUTE = 4;

    /**
     * enum for JCB
     */
    static final int JCB = 5;

    /**
     * enum for Mastercard
     */
    static final int MASTERCARD = 6;

    /**
     * enum for insufficient digits
     */
    static final int NOT_ENOUGH_DIGITS = -3;

    /**
     * enum for too many digits
     */
    static final int TOO_MANY_DIGITS = -2;

    /**
     * enum for unknown vendor
     */
    static final int UNKNOWN_VENDOR = -1;

    /**
     * enum for Visa
     */
    static final int VISA = 7;

    /**
     * Used to speed up findMatchingRange by caching the last hit.
     */
    private static int cachedLastFind = 0;

    /**
     * ranges of credit card number that belong to each company. buildRanges
     * initialises.
     */
    private static LCR[] ranges;

    /**
     * used by vendorToString to describe the enumerations
     */
    private static final String[] vendors =
            {"Error: not enough digits",
             "Error: too many digits",
             "Error: unknown credit card company",
             "dummy",
             "Amex",
             "Diners/Carte Blanche",
             "Discover",
             "enRoute",
             "JCB",
             "MasterCard",
             "Visa"};

    // -------------------------- STATIC METHODS --------------------------

    static
        {
        // now that all enum constants defined
        buildRanges();
        }

    /**
     * build table of which ranges of credit card number belong to which vendor
     */
    private static void buildRanges()
        {
        // careful, no lead zeros allowed
        // low high len vendor mod-10?
        ranges =
                new LCR[] {new LCR( 4000000000000L,
                                    4999999999999L,
                                    13,
                                    VISA,
                                    true ),
                           new LCR( 30000000000000L,
                                    30599999999999L,
                                    14,
                                    DINERS,
                                    true ),
                           new LCR( 36000000000000L,
                                    36999999999999L,
                                    14,
                                    DINERS,
                                    true ),
                           new LCR( 38000000000000L,
                                    38999999999999L,
                                    14,
                                    DINERS,
                                    true ),
                           new LCR( 180000000000000L,
                                    180099999999999L,
                                    15,
                                    JCB,
                                    true ),
                           new LCR( 201400000000000L,
                                    201499999999999L,
                                    15,
                                    ENROUTE,
                                    false ),
                           new LCR( 213100000000000L,
                                    213199999999999L,
                                    15,
                                    JCB,
                                    true ),
                           new LCR( 214900000000000L,
                                    214999999999999L,
                                    15,
                                    ENROUTE,
                                    false ),
                           new LCR( 340000000000000L,
                                    349999999999999L,
                                    15,
                                    AMEX,
                                    true ),
                           new LCR( 370000000000000L,
                                    379999999999999L,
                                    15,
                                    AMEX,
                                    true ),
                           new LCR( 3000000000000000L,
                                    3999999999999999L,
                                    16,
                                    JCB,
                                    true ),
                           new LCR( 4000000000000000L,
                                    4999999999999999L,
                                    16,
                                    VISA,
                                    true ),
                           new LCR( 5100000000000000L,
                                    5599999999999999L,
                                    16,
                                    MASTERCARD,
                                    true ),
                           new LCR( 6011000000000000L,
                                    6011999999999999L,
                                    16,
                                    DISCOVER,
                                    true )}; // end table initialisation
        }

    /**
     * Finds a matching range in the ranges array for a given creditCardNumber.
     *
     * @param creditCardNumber number on card.
     *
     * @return index of matching range, or NOT_ENOUGH_DIGITS or UNKNOWN_VENDOR
     *         on failure.
     */
    protected static int findMatchingRange( long creditCardNumber )
        {
        if ( creditCardNumber < 1000000000000L )
            {
            return NOT_ENOUGH_DIGITS;
            }
        if ( creditCardNumber > 9999999999999999L )
            {
            return TOO_MANY_DIGITS;
            }
        // check the cached index first, where we last found a number.
        if ( ranges[ cachedLastFind ].low <= creditCardNumber
             && creditCardNumber <= ranges[ cachedLastFind ].high )
            {
            return cachedLastFind;
            }
        for ( int i = 0; i < ranges.length; i++ )
            {
            if ( ranges[ i ].low <= creditCardNumber
                 && creditCardNumber <= ranges[ i ].high )
                {
                // we have a match
                cachedLastFind = i;
                return i;
                }
            } // end for
        return UNKNOWN_VENDOR;
        } // end findMatchingRange

    
    public static boolean isValid(String strCreditCardNumber) {
    	return isValid(toLong(strCreditCardNumber, 0L));
    }

	private static long toLong(String strCreditCardNumber, long defaultValue) {
    	
    	if(strCreditCardNumber==null) return defaultValue;
        
        // strip commas, spaces, + AND -
        StringBuffer sb = new StringBuffer( strCreditCardNumber.length() );
        for (int i = 0; i < strCreditCardNumber.length(); i++ ) {
            char c = strCreditCardNumber.charAt( i );
            if(!StringUtil.isWhiteSpace(c) && c!=',' && c!='+' && c!='-')
            	sb.append( c );
        }
        long num=(long) Caster.toDoubleValue(sb.toString(),0L);
        if(num==0L) return defaultValue;
    	
    	return num;
    }
    
    /**
     * Determine if the credit card number is valid, i.e. has good prefix and
     * checkdigit. Does _not_ ask the credit card company if this card has been
     * issued or is in good standing.
     *
     * @param creditCardNumber number on card.
     *
     * @return true if card number is good.
     */
    public static boolean isValid( long creditCardNumber ) {
        int i = findMatchingRange( creditCardNumber );
        if ( i < 0 ) {
            return false;
        }
        //else {
            // we have a match
            if ( ranges[ i ].hasCheckDigit )
                {
                // there is a checkdigit to be validated
                /*
                 * Manual method MOD 10 checkdigit 706-511-227 7 0 6 5 1 1 2 2 7
                 * 2 * 2 * 2 * 2 --------------------------------- 7 + 0 + 6
                 * +1+0+ 1 + 2 + 2 + 4 = 23 23 MOD 10 = 3 10 - 3 = 7 -- the
                 * check digit Note digits of multiplication results must be
                 * added before sum. Computer Method MOD 10 checkdigit
                 * 706-511-227 7 0 6 5 1 1 2 2 7 Z Z Z Z
                 * --------------------------------- 7 + 0 + 6 + 1 + 1 + 2 + 2 +
                 * 4 + 7 = 30 30 MOD 10 had better = 0
                 */
                long number = creditCardNumber;
                int checksum = 0;
                // work right to left
                for ( int place = 0; place < 16; place++ )
                    {
                    int digit = (int) ( number % 10 );
                    number /= 10;
                    if ( ( place & 1 ) == 0 )
                        {
                        // even position (0-based from right), just add digit
                        checksum += digit;
                        }
                    else
                        { // odd position (0-based from right), must double
                        // and add
                        checksum += z( digit );
                        }
                    if ( number == 0 )
                        {
                        break;
                        }
                    } // end for
                // good checksum should be 0 mod 10
                return ( checksum % 10 ) == 0;
                }
            return true; // no checksum needed
                
            //} // end if have match
        } // end isValid


    
    /**
     * Determine the credit card company. Does NOT validate checkdigit.
     *
     * @param creditCardNumber number on card.
     *
     * @return credit card vendor enumeration constant.
     */
    public static int recognizeVendor( long creditCardNumber )
        {
        int i = findMatchingRange( creditCardNumber );
        if ( i < 0 )
            {
            return i;
            }
        return ranges[ i ].vendor;
            
        } // end recognize

    // From http://www.icverify.com/
    // Vendor Prefix len checkdigit
    // MASTERCARD 51-55 16 mod 10
    // VISA 4 13, 16 mod 10
    // AMEX 34,37 15 mod 10
    // Diners Club/
    // Carte Blanche
    // 300-305 14
    // 36 14
    // 38 14 mod 10
    // Discover 6011 16 mod 10
    // enRoute 2014 15
    // 2149 15 any
    // JCB 3 16 mod 10
    // JCB 2131 15
    // 1800 15 mod 10

    public static String toCreditcard(String strCreditCardNumber) throws ExpressionException {
    	long number=toLong(strCreditCardNumber, -1);
    	if(number==-1) throw new ExpressionException("invalid creditcard number ["+strCreditCardNumber+"]");
    	return toPrettyString(number);
    }
    public static String toCreditcard(String strCreditCardNumber, String defaultValue) {
    	long number=toLong(strCreditCardNumber, -1);
    	if(number==-1) return defaultValue;
    	return toPrettyString(number);
    }
    
    /**
     * Convert a creditCardNumber as long to a formatted String. Currently it
     * breaks 16-digit numbers into groups of 4.
     *
     * @param creditCardNumber number on card.
     *
     * @return String representation of the credit card number.
     */
    private static String toPrettyString( long creditCardNumber )
        {
        String plain = Long.toString( creditCardNumber );
        // int i = findMatchingRange(creditCardNumber);
        int length = plain.length();

        switch ( length )
            {
            case 12 :
                // 12 pattern 3-3-3-3
                return plain.substring( 0, 3 )
                       + ' '
                       + plain.substring( 3, 6 )
                       + ' '
                       + plain.substring( 6, 9 )
                       + ' '
                       + plain.substring( 9, 12 );

            case 13 :
                // 13 pattern 4-3-3-3
                return plain.substring( 0, 4 )
                       + ' '
                       + plain.substring( 4, 7 )
                       + ' '
                       + plain.substring( 7, 10 )
                       + ' '
                       + plain.substring( 10, 13 );

            case 14 :
                // 14 pattern 2-4-4-4
                return plain.substring( 0, 2 )
                       + ' '
                       + plain.substring( 2, 6 )
                       + ' '
                       + plain.substring( 6, 10 )
                       + ' '
                       + plain.substring( 10, 14 );

            case 15 :
                // 15 pattern 3-4-4-4
                return plain.substring( 0, 3 )
                       + ' '
                       + plain.substring( 3, 7 )
                       + ' '
                       + plain.substring( 7, 11 )
                       + ' '
                       + plain.substring( 11, 15 );

            case 16 :
                // 16 pattern 4-4-4-4
                return plain.substring( 0, 4 )
                       + ' '
                       + plain.substring( 4, 8 )
                       + ' '
                       + plain.substring( 8, 12 )
                       + ' '
                       + plain.substring( 12, 16 );

            case 17 :
                // 17 pattern 1-4-4-4-4
                return plain.substring( 0, 1 )
                       + ' '
                       + plain.substring( 1, 5 )
                       + ' '
                       + plain.substring( 5, 9 )
                       + ' '
                       + plain.substring( 9, 13 )
                       + ' '
                       + plain.substring( 13, 17 );

            default :
                // 0..11, 18+ digits long
                // plain
                return plain;
            } // end switch
        } // end toPrettyString

    /**
     * Converts a vendor index enumeration to the equivalent words. It will
     * trigger an ArrayIndexOutOfBoundsException if you feed it an illegal
     * value.
     *
     * @param vendorEnum e.g. AMEX, UNKNOWN_VENDOR, TOO_MANY_DIGITS
     *
     * @return equivalent string in words, e.g. "Amex" "Error: unknown vendor".
     */
    public static String vendorToString( int vendorEnum )
        {
        return vendors[ vendorEnum - NOT_ENOUGH_DIGITS ];
        } // end vendorToString

    /**
     * used in computing checksums, doubles and adds resulting digits.
     *
     * @param digit the digit to be doubled, and digit summed.
     *
     * @return // 0->0 1->2 2->4 3->6 4->8 5->1 6->3 7->5 8->7 9->9
     */
    private static int z( int digit )	{
        if ( digit == 0 )	{
            return 0;
        }
        return ( digit * 2 - 1 ) % 9 + 1;
    }

    // --------------------------- main() method ---------------------------

    /**
     * Test driver
     *
     * @param args not used
     */
    public static void main( String[] args )
        {
        if ( DEBUGGING )
            {
        	
        	

            System.out.println( isValid( "0" ) ); 
            System.out.println( isValid( "4000000000006" ) ); 
            System.out.println( isValid( "40000000-000,06" ) ); 
            
            System.out.println( isValid( 0 ) ); 
            System.out.println( isValid( 6010222233334444L ) ); // false
            System.out.println( isValid( 4000000000000L ) ); // false
            System.out.println( isValid( 4000000000006L ) ); // true
            System.out.println( isValid( 4000000000009L ) ); // false
            System.out.println( isValid( 4999999999999L ) ); // false
            System.out.println( isValid( 378888888888858L ) ); // true, Amex
            System.out.println( isValid( 4888888888888838L ) ); // true, Visa;
            System.out.println( isValid( 5588888888888838L ) ); // true, MC
            System.out.println( isValid( 6011222233334444L ) ); // true, Dicover

            } // end if debugging
        } // end main
} // end class CreditCard

/**
 * Describes a single Legal Card Range
 */
class LCR {

    // ------------------------------ FIELDS ------------------------------

    /**
     * does this range have a MOD-10 checkdigit?
     */
    public boolean hasCheckDigit;

    /**
     * low and high bounds on range covered by this vendor
     */
    public long high;

    /**
     * how many digits in this type of number.
     */
    public int length;

    /**
     * low bounds on range covered by this vendor
     */
    public long low;

    /**
     * enumeration credit card service
     */
    public int vendor;

    // --------------------------- CONSTRUCTORS ---------------------------

    /**
     * public constructor
     *
     * @param low           lowest credit card number in range.
     * @param high          highest credit card number in range
     * @param length        length of number in digits
     * @param vendor        enum constant for vendor
     * @param hasCheckDigit true if uses mod 10 check digit.
     */
    public LCR( long low,
                long high,
                int length,
                int vendor,
                boolean hasCheckDigit )
        {
        this.low = low;
        this.high = high;
        this.length = length;
        this.vendor = vendor;
        this.hasCheckDigit = hasCheckDigit;
        } // end public constructor
} // end class LCR
