/*******************************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ******************************************************************************/
package gov.va.med.fw.rule;

// Java classes
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.util.TimeZone;
import java.math.BigDecimal;

// Library classes
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

// Framework classes
import gov.va.med.fw.security.SecurityContext;
import gov.va.med.fw.security.SecurityContextHelper;

// EDB classes

/**
 * Project: Framework
 * 
 * @author DNS   LEV
 * @version 1.0
 */
public class SimpleRuleMethod implements RuleMethod {
    /**
     * An instance of serialVersionUID
     */
    private static final long serialVersionUID = 8020776505735217870L;

    /**
     * A logger to log information
     */
    protected Log logger = LogFactory.getLog(getClass());

    public SimpleRuleMethod() {
        super();
    }

    /**
     * Compares the 2 calendars for equality. This method allows a caller to
     * pass in a significant calendar field to round off before a comparison of
     * the 2 calendars take place.
     * 
     * @param arg1
     *            A calendar to compare
     * @param arg2
     *            A calendar to compare
     * @return True if 2 objects are equal. False otherwise
     */
    public boolean isCalendarEqual(Calendar arg1, Calendar arg2, int field)
            throws RuleException {
        boolean compare = false;

        checkCalendarField(field, "Invalid calendar field");

        if (arg1 == arg2) {
            compare = true;
        } else if (arg1 != null && arg2 != null) {

            Calendar rounded1 = DateUtils.round(arg1, field);
            Calendar rounded2 = DateUtils.round(arg2, field);
            compare = rounded1.equals(rounded2);
            if (logger.isDebugEnabled()) {
                logger.debug(" Arg1: "
                        + ((arg1 != null) ? arg1.getTime() : "") + " Arg2: "
                        + ((arg2 != null) ? arg2.getTime() : ""));
                logger.debug(" Returned value: " + compare);
            }
        }
        return compare;
    }

    /**
     * Compares two objects for equality, where either one or both objects may
     * be null. For instance:
     * 
     * ObjectUtils.equals(null, null) = true ObjectUtils.equals(null, "") =
     * false ObjectUtils.equals("", null) = false ObjectUtils.equals("", "") =
     * true ObjectUtils.equals(Boolean.TRUE, null) = false
     * ObjectUtils.equals(Boolean.TRUE, "true") = false
     * ObjectUtils.equals(Boolean.TRUE, Boolean.TRUE) = true
     * ObjectUtils.equals(Boolean.TRUE, Boolean.FALSE) = false
     * 
     * @param arg1
     *            An object to compare
     * @param arg2
     *            An object to compare
     * @return True if 2 objects are equal. False otherwise
     */
    public boolean isEqual(Object arg1, Object arg2) {
        boolean compare = false;

        // If 2 objects are string, ignore cases in comparison
        if (arg1 instanceof String && arg2 instanceof String && arg1 != null
                && arg2 != null) {
            compare = ((String) arg1).equalsIgnoreCase((String) arg2);
        } else if (arg1 != null && arg2 != null
                && Date.class.isAssignableFrom(arg1.getClass())) {
            Date lhsDate = (Date) arg1;
            Date rhsDate = (Date) arg2;
            compare = (lhsDate.getTime() == rhsDate.getTime());
        } else {
            compare = ObjectUtils.equals(arg1, arg2);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Arg1: " + ((arg1 != null) ? arg1.toString() : null)
                    + " Arg2: " + ((arg2 != null) ? arg2.toString() : null));
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    public boolean isNull(Object obj) {
        logger.debug("is null: " + (obj == null));
        return obj == null;
    }

    public BigDecimal add(BigDecimal a, BigDecimal b) {
        BigDecimal c = a.add(b).setScale(2, BigDecimal.ROUND_HALF_UP);
        logger.debug(a + " + " + b + " = " + c);
        return c;
    }

    public BigDecimal percentageOf(BigDecimal a, double percentage) {
        BigDecimal percentDecimal = new BigDecimal(percentage / 100).setScale(
                2, BigDecimal.ROUND_HALF_UP);
        BigDecimal b = a.multiply(percentDecimal).setScale(2,
                BigDecimal.ROUND_HALF_UP);
        logger.debug(a + " * " + percentDecimal + " = " + b);
        return b;
    }

    /**
     * Compares a big decimal and a number
     * 
     * @param arg1
     *            A big decima to compare
     * @param arg2
     *            A number to compare
     * @return True if 2 objects are equal. False otherwise
     */
    public boolean isEqual(BigDecimal arg1, int arg2) {
        boolean compare = this.isEqual(toInteger(arg1), new Integer(arg2));

        return compare;
    }

    /**
     * Checks if a value is within a range
     * 
     * @param lower
     *            A lower bound
     * @param upper
     *            An upper bound
     * @return True if a value is within a range. false otherwise
     */
    public boolean isInRange(Integer value, int lower, int upper) {
        boolean inRange = false;
        if (value != null) {
            inRange = ((lower <= value.intValue() && value.intValue() <= upper) ? true
                    : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Lower bound: " + lower + " Upper bound: " + upper);
            logger.debug(" Returned value: " + inRange);
        }
        return inRange;
    }

    /**
     * Checks if a value is within a range
     * 
     * @param lower
     *            A lower bound
     * @param upper
     *            An upper bound
     * @return True if a value is within a range. false otherwise
     */
    public boolean isInRange(BigDecimal value, double lower, double upper) {
        boolean inRange = false;
        if (value != null) {
            inRange = ((lower <= value.doubleValue() && value.doubleValue() <= upper) ? true
                    : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Lower bound: " + lower + " Upper bound: " + upper);
            logger.debug(" Returned value: " + inRange);
        }
        return inRange;
    }

    /**
     * Checks if a value is one of the items in a collection
     * 
     * @param value
     *            An object to check
     * @param pattern
     *            A collection of values to compare
     * @return True if an object is in a collection
     */
    public boolean isPartOf(String value, String pattern) {

        boolean compare = false;
        if (value != null && pattern != null) {
            compare = (pattern.indexOf(value) != -1);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" String to search " + value);
            logger.debug(" String to compare " + pattern);
        }
        return compare;
    }

    /**
     * Checks if a collection is either empty or null
     * 
     * @param collection
     *            A collection to check for empty and null
     * @return True if a collection is empty or null
     */
    public boolean isEmpty(Collection collection) {

        // if a collection is null, it is considered empty
        boolean empty = true;

        if (collection != null) {
            empty = collection.isEmpty();
        }
        return empty;
    }

    /**
     * Checks if a value is less than an upper bound
     * 
     * @param upper
     *            An upper bound
     * @return True if a value is less than an upper bound. false otherwise
     */
    public boolean isLessThan(Integer value, int upper) {
        boolean compare = false;
        if (value != null) {
            compare = (value.intValue() < upper ? true : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Upper bound: " + upper);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    /**
     * Checks if a value is greater than a lower bound
     * 
     * @param lower
     *            A lower bound
     * @return True if a value is greater than a lower bound. false otherwise
     */
    public boolean isGreaterThan(Integer value, int lower) {
        boolean compare = false;
        if (value != null) {
            compare = (value.intValue() > lower ? true : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Lower bound: " + lower);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    /**
     * Checks if a value is greater than a lower bound
     * 
     * @param lower
     *            A lower bound
     * @return True if a value is greater than a lower bound. false otherwise
     */
    public boolean isGreaterThan(BigDecimal value, BigDecimal lower) {
        boolean compare = false;
        if (value != null && lower != null) {
            compare = value.compareTo(lower) == 1;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" value: " + value);
            logger.debug(" Lower bound: " + lower);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    /**
     * Checks if a value is greater than a lower bound
     * 
     * @param lower
     *            A lower bound
     * @return True if a value is greater than a lower bound. false otherwise
     */
    public boolean isGreaterThan(BigDecimal value, Integer lower) {
        boolean compare = false;

        if (value != null && lower != null) {
            BigDecimal lowerBD = new BigDecimal(lower.intValue());
            compare = value.compareTo(lowerBD) == 1;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" value: " + value);
            logger.debug(" Lower bound: " + lower);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    /**
     * Checks if a value is greater than a lower bound
     * 
     * @param lower
     *            A lower bound
     * @return True if a value is greater than a lower bound. false otherwise
     */
    public boolean isGreaterThan(Integer value, BigDecimal lower) {
        boolean compare = false;

        if (value != null && lower != null) {
            BigDecimal valueBD = new BigDecimal(value.intValue());
            compare = valueBD.compareTo(lower) == 1;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" value: " + value);
            logger.debug(" Lower bound: " + lower);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    /**
     * Checks if a value is greater than a lower bound
     * 
     * @param lower
     *            A lower bound
     * @return True if a value is greater than a lower bound. false otherwise
     */
    public boolean isGreaterThan(Integer value, Integer lower) {
        boolean compare = false;
        if (value != null && lower != null) {
            int lowerInt = lower.intValue();
            compare = (value.intValue() > lowerInt ? true : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Lower bound: " + lower);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    public boolean isLessThan(BigDecimal value, BigDecimal upper) {
        boolean compare = false;
        if (value != null && upper != null) {
            compare = value.compareTo(upper) == -1;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" value: " + value);
            logger.debug(" Upper bound: " + upper);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    /*
     * @see gov.va.med.fw.rule.RuleMethod#isDateBetween(java.util.Date,
     *      java.util.Date, java.util.Date)
     */
    public boolean isDateBetween(Date dateToCheck, Date lowerDate,
            Date upperDate) {
        if (dateToCheck == null)
            return false;

        // Get the time to check
        long checkTime = dateToCheck.getTime();

        // Check the lower date if present
        if (lowerDate != null) {
            long lowTime = lowerDate.getTime();
            if (checkTime < lowTime) {
                return false;
            }
        }

        // Check the upper date is present
        if (upperDate != null) {
            long upperTime = upperDate.getTime();
            if (checkTime > upperTime) {
                return false;
            }
        }

        // The date is between the other dates
        return true;
    }

    /**
     * Checks if two dates are of same day
     * 
     * @return True if a dates are same day. false otherwise
     */
    public boolean isSameDay(Date value1, Date value2) {
        if (value1 != null && value2 != null) {
            return DateUtils.isSameDay(value1, value2);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Value 1: " + value1);
            logger.debug(" Value 2: " + value2);
        }
        return false;
    }

    /**
     * Checks if a date is before the specific date
     * 
     * @param upper
     *            A date to compare
     * @return True if a date is before a threshold. false otherwise
     */
    public boolean isBefore(Date value, Date upper) {

        boolean compare = false;
        if (value != null && upper != null) {
            compare = (value.before(upper) ? true : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Upper bound: " + upper);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    /**
     * Checks if a date is after the specific date
     * 
     * @param lower
     *            A date to compare
     * @return True if a date is after a threshold. false otherwise
     */
    public boolean isAfter(Date value, Date lower) {

        boolean compare = false;
        if (value != null && lower != null) {
            compare = (value.after(lower) ? true : false);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(" Upper bound: " + lower);
            logger.debug(" Returned value: " + compare);
        }
        return compare;
    }

    public Date toDate(String value) throws ParseException {
        Validate.notNull(value, "A date string must not be NULL");
        Date parsed = toDate(value, SimpleDateFormat.SHORT);
        if (logger.isDebugEnabled()) {
            logger.debug(" Input date string: " + value);
            logger.debug(" Parsed date: " + parsed);
        }
        return parsed;
    }

    public Date toDate(String value, int pattern) throws ParseException {
        Validate.notNull(value, "A date string must not be NULL");
        Date parsed = SimpleDateFormat.getDateInstance(pattern).parse(value);
        if (logger.isDebugEnabled()) {
            logger.debug(" Input date string: " + value);
            logger.debug(" Parsed date: " + parsed);
        }
        return parsed;
    }

    public Date getCurrentDate() {
        Date current = Calendar.getInstance().getTime();
        if (logger.isDebugEnabled()) {
            logger.debug(" The current date: " + current);
        }
        return current;
    }

    public String getLoggedInUser() {
        SecurityContext securityContext = SecurityContextHelper
                .getSecurityContext();
        return (securityContext != null) ? securityContext.getUserName() : null;
    }

    /**
     * Returns the collection size. 0 if a collection is null.
     * 
     * @param collection
     *            A collection to get a size
     * @return 0 if a collection is null. Otherwise, a size() is called
     */
    public int getSize(Collection collection) {
        return collection == null ? 0 : collection.size();
    }

    /**
     * Converts a date to a calendar using the specific timezone.
     * 
     * @param date
     *            A date to convert to calendar
     * @return Calendar A calender for the specific date
     * @throws IllegalArgumentException
     *             Thrown if missing required date
     */
    public Calendar toCalendar(Date date, TimeZone timezone) {

        Validate.notNull(date, "A date object must not be NULL");

        Calendar calendar = Calendar.getInstance();
        if (timezone == null) {
            // Set a default time zone
            calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
        } else {
            calendar.setTimeZone(timezone);
        }
        calendar.setTime(date);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);

        return calendar;
    }

    /**
     * Converts a date to a calendar using the default system timezone.
     * 
     * @param date
     *            A date to convert to calendar
     * @return Calendar A calender for the specific date
     * @throws IllegalArgumentException
     *             Thrown if missing required date
     */
    public Calendar toCalendar(Date date) {
        return toCalendar(date, null);
    }

    /**
     * Converts a Number to an Integer
     * 
     * @see gov.va.med.fw.rule.RuleMethod#toInteger(java.lang.Number)
     */
    public Integer toInteger(Number value) {

        int converted = (value != null ? value.intValue() : 0);
        return toInteger(converted);
    }

    /**
     * Converts a Number to an Integer
     * 
     * @see gov.va.med.fw.rule.RuleMethod#toInteger(java.lang.Number)
     */
    public Integer toInteger(int value) {
        Integer converted = new Integer(value);
        return converted;
    }

    /**
     * Throws an IllegalArgumentException exception if a field is not a valid
     * Calendar field.
     * 
     * @param field
     *            A valid calendar field
     * @param message
     *            An exception message
     */
    protected void checkCalendarField(int field, String message)
            throws RuleException {
        if (field != Calendar.SECOND && field != Calendar.MINUTE
                && field != Calendar.HOUR && field != Calendar.DATE
                && field != Calendar.MONTH && field != Calendar.YEAR) {
            throw new RuleException(message);
        }
    }
}