package lu.tudor.santec.gecamed.billing.utils.rules;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lu.tudor.santec.gecamed.billing.ejb.entity.beans.Act;
import lu.tudor.santec.gecamed.billing.ejb.entity.beans.Invoice;
import lu.tudor.santec.gecamed.billing.ejb.entity.beans.KeyValue;
import lu.tudor.santec.gecamed.billing.ejb.entity.beans.RateIndex;
import lu.tudor.santec.gecamed.billing.ejb.entity.beans.Suffix;
import lu.tudor.santec.gecamed.billing.utils.BillingAdminSettings;
import lu.tudor.santec.gecamed.core.utils.GECAMedUtils;
import lu.tudor.santec.gecamed.patient.ejb.entity.beans.HospitalisationClass;

/**
 * @author jens.ferring(at)tudor.lu
 * 
 * In this class all cumulations for the billing rules according to the 
 * CNS rules are done (as far as implemented).
 * 
 * @version
 * <br>$Log: Cumulations.java,v $
 * <br>Revision 1.7  2013-05-24 12:17:03  ferring
 * <br>Hospitalisation rule fixed
 * <br>
 * <br>Revision 1.6  2013-04-23 12:27:05  ferring
 * <br>Billing rules fixed: 1. 2 days of hospitalisation
 * <br>
 * <br>Revision 1.5  2013-04-02 12:20:32  ferring
 * <br>debug logging removed
 * <br>
 * <br>Revision 1.4  2013-03-25 12:59:50  ferring
 * <br>obstetricsCumulation does not use removed acts anymore
 * <br>
 * <br>Revision 1.3  2013-03-22 10:27:03  ferring
 * <br>UnionActs added and billing bugs fixed (since last not released commit)
 * <br>
 * <br>Revision 1.2  2013-03-20 09:10:25  ferring
 * <br>Comments added
 * <br>
 * <br>Revision 1.1  2013-03-14 10:40:20  ferring
 * <br>Billing rules corrected
 * <br>
 * <br>Revision 1.1  2013-03-01 11:15:12  ferring
 * <br>Parts of billing rule logic moved to helper classes.
 * <br>Special cumulation rule for gynaecologists implemented.
 * <br>
 */
public class Cumulations
{
	/* ======================================== */
	// 		CONSTANTS
	/* ======================================== */
	
	private static int	FREE_HOSPITALISATION_CUMULATION_DAYS	= 2;
	
	/**
	 * A mapping that maps codes to a list of codes, which 
	 */
	public static final Map<String, Collection<String>>	unionCumulationActs				= new HashMap<String, Collection<String>>();
	
	/**
	 * A set of all acts that can be cumulated with everything without any restrictions
	 */
	private static final Set<String>					unlimitedCumulativeActs			= new HashSet<String>();
	
	/**
	 * A set of all acts that can be cumulated with everything without any restrictions, if session mode is activated
	 */
	private static final Set<String>					unlimitedSessionCumulativeActs	= new HashSet<String>();
	
	
	
	/* ======================================== */
	// 		INITIALIZATION
	/* ======================================== */
	
	static 
	{
		initialize();
	}
	
	
	
	/* ======================================== */
	// 		CLASS BODY
	/* ======================================== */
	
	public static void executePreCumulativeOperations (RulesObjectsHolder roh)
	{
		roh.printActList("Before Cumulations", "CUMULATIONS START!");
		
		hospitalizationDay1And2Cumulation(roh);
		roh.printActList("After Cumulations.hospitalizationDay1And2Cumulation", "in the first 2 days of a hospitalisation, for some physician groups, all acts can be cumulated with each other (see nomenclature art. 10 §1 point 7)");
		
		UnionAct.insertUnionActs(roh);
		roh.printActList("After UnionAct.insertUnionActs", "See T_8_1 nomenclature Art. 9 §4");
	}
	
	/**
	 * This is the main method, that triggers all the cumulation 
	 * functions for dentists in the appropriate order.
	 */
	public static void executeDentalCumulations (RulesObjectsHolder roh)
	{
		if (roh.getInvoice().getActs().size() == 0)
			// no dental acts on this invoice
			return;
		
		roh.discardSortedActs();
		
		dentists3rdChapterCumulation(roh);
		roh.printActList("After Cumulations.dentists3rdChapterCumulation", "Special Cumulations for dental technical acts of the chapter 3");
		
		// TODO: Implement rejectTechnicalOrGeneralDentalActs
//		rejectTechnicalOrGeneralDentalActs(roh);
//		roh.printActList("After Cumulations.rejectTechnicalOrGeneralDentalActs", "");
		
		/** TODO: Still left to implement:
		 * Art. 10 1): CACs can be cumulated with all consultations, except the "DC2"
		 * Art. 10 2): DG_3 can be cumulated with DG & DT
		 * Art. 10 3): DG_2_1 can be cumulated with all DT and DG_5
		 * Art. 10 5): DG_6 can be cumulated with all DT
		 * 
		 * and much more ...
		 */
		
		roh.getInvoice().monetize();
		roh.printActList("After Dental Cumulations", "Dental Stuff DONE!");
	}

	/**
	 * This is the main method, that triggers all the cumulation 
	 * functions for physicians in the appropriate order.
	 */
	public static void executeMedicalCumulations (RulesObjectsHolder roh)
	{
		roh.printActList("Before General Cumulations", "");
		
		if (roh.getInvoice().getActs().size() == 0)
			// no medical acts on this invoice
			return;
		
		roh.discardSortedActs();
		roh.printActList("After discardSortedActs", "");
		/* **************************************** */
		//	Call the functions
		/* **************************************** */
		
		obstetricsCumulation(roh);
		roh.printActList("After Cumulations.obstetricsCumulation", "special cumulation rules for the gynecologists (100% | 100% | 50% | 50% | 0%)");
		
		defaultCumulation(roh);
		roh.printActList("After Cumulations.defaultCumulations", "Apply the default cumulation (100|50|50) on the technical acts of each day");
		
		checkRentalActs(roh);
		roh.printActList("After Cumulations.checkRentalActs", "Remove all rental acts, that haven't their base act set");
		
		rejectTechnicalOrGeneralMedicalActs(roh);
		roh.printActList("After Cumulations.rejectTechnicalOrGeneralActs", "Removes either the general or technical acts (as far as not compatible with each other) from the invoice");
		
		//must be after rejectTechnicalOrGeneralActs, because otherwise this will reset the changes made here
		anesthetistHospitalizationFeeCheck(roh);
		roh.printActList("After Cumulations.anesthetistHospitalizationFeeCheck", "This method checks, whether the anesthetic hospitalization fees respect the 24-hours minimum, If there are only 2 hospitalization fees for the anesthetists, the second must be at least 24 hours after the first. Otherwise it has to be set to 0.");
		
		roh.getInvoice().monetize();
		roh.printActList("After Medical Cumulations", "Medical Stuff Done!");
	}
	
	
	public static void executePostCumulativeOperations (RulesObjectsHolder roh)
	{
		UnionAct.replaceUnionActs(roh.getInvoice());
		roh.printActList("After UnionAct.replaceUnionActs", "");
	}


	/**
	 * This method checks, whether the anesthetic hospitalization fees respect the 24-hours minimum.<br>
	 * If there are only 2 hospitalization fees for the anesthetists, the second must be at least 24 hours
	 * after the first. Otherwise it has to be set to 0.
	 * 
	 * @param roh The RulesObjectHolder containing all important invoice and rule data.
	 */
	public static void anesthetistHospitalizationFeeCheck (RulesObjectsHolder roh)
	{
		Invoice			invoice		= roh.getInvoice();
		List<Act>		fActs		= new ArrayList<Act>();
		Act				lastAct;
		
		
		if (HospitalisationClass.c_Ambulant.equals(invoice.getHospitalisationClass().getAcronym()))
			// not a hospitalization invoice
			return;
		
		// filter all anesthetic hospitalization fees
		lastAct = null;
		for (ActSorter actsOfDay : roh.getActsOfSessions().values())
		{
			for (Act act : actsOfDay)
			{
				if (RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_ANESTHETIC_HOSPITALIZATION_FEES, act))
				{
					if (lastAct != null && lastAct.getPerformedDay().equals(act.getPerformedDay()))
					{
						// keep only 1 act per day - only the latest one
						removeAct(fActs.remove(fActs.size() - 1));
					}
					fActs.add(act);
					lastAct	= act;
					
					if (fActs.size() > 2)
						// this rule is only valid, if there is no 3rd day
						return;
				}
			}
		}
		
		if (fActs.size() == 2)
		{
			/* As the rule only counts for the 2nd fee and only if there is no 3rd fee
			 * and as fActs contains only 1 act of each day, this rule is only executed
			 * if the size of fActs is exactly 2
			 */
			Act			_1stAct	= fActs.get(0);
			Act			_2ndAct	= fActs.get(1);
			Calendar	_1stCal	= _1stAct.getPerformedCalendar();
			Calendar	_2ndCal	= _2ndAct.getPerformedCalendar();
			int			_1stMin	= _1stCal.get(Calendar.HOUR_OF_DAY) * 60 + _1stCal.get(Calendar.MINUTE);
			int			_2ndMin	= _2ndCal.get(Calendar.HOUR_OF_DAY) * 60 + _2ndCal.get(Calendar.MINUTE);
			
			if (GECAMedUtils.getDifferenceInDays(_1stCal, _2ndCal, true) == 1
					&& _1stMin > _2ndMin)
			{
				/* The fee on the 2nd day is before the one on the first day 
				 * -> 24 hours rule not respected, remove it
				 */
				removeAct(_2ndAct);
			}
		}
	}
	
	
	/**
	 * Apply the default cumulation (100|50|50) on the technical acts of each day
	 * 
	 * @param roh The RulesObjectHolder containing all important invoice and rule data.
	 */
	public static void defaultCumulation (RulesObjectsHolder roh)
	{
		List<Act>				technicalActs	= new LinkedList<Act>();
		boolean					changed			= false;
		int						noOfCumulatedActs;
		
		
		for (List<Act> actsOfDay : roh.getSortedActs())
		{
			if (actsOfDay == null
					|| actsOfDay.isEmpty()
					|| !roh.isDayRuleOption(actsOfDay.get(0).getSessionDate(), RulesObjectsHolder.OPTION_PERFORM_DEFAULT_CUMULATION))
			{
				// no acts for this day or the option, to perform the default cumulation is not set for this day
				continue;
			}
			
			technicalActs.clear();
			for (Act act : actsOfDay)
			{
				// filter all technical acts of this day - without rental or material acts
				if (!act.isRental()
						&& !act.isMaterial()
						&& act.getQuantity() > 0
						&& !RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_GEN_ACT, act.getCode())
						&& !isCumulativeWithoutLimitation(act.getCode(), roh))
				{
					// normal technical act found
					technicalActs.add(act);
				}
			}
			
			if (technicalActs.isEmpty())
				continue;
			
			// sort the technical acts ...
			technicalActs		= ActSorter.sort(technicalActs, roh, 1.0, 0.5, 0.5);
			// ... and cumulate them correctly
			noOfCumulatedActs	= 0;
			for (Act act : technicalActs)
			{
//				System.out.println(noOfCumulatedActs + " " + act);
				if (noOfCumulatedActs < 1) {
					cumulateAct(act, false);					
				} else if (noOfCumulatedActs < 3) {
					cumulateAct(act, true);					
				} else {
					removeAct(act);					
				}
				noOfCumulatedActs++;
			}
		}
		
		if (changed)
			// remove the sortedActs, as something was changed
			roh.discardSortedActs();
	}
	
	
	/**
	 * During the first 2 days of a hospitalisation, for some physician groups, all acts (maybe just technical and F-acts - nobody could tell us for sure) 
	 * can be cumulated with each other (see nomenclature art. 10 §1 point 7).<br>
	 * Anyway, there is an option, telling the rules, whether
	 * <ul>
	 * <li>the default rules shall be applied</li>
	 * <li>the acts can be cumulated without limitation (if an F-code is present)</li>
	 * <li>the acts can be cumulated without limitation (even if no F-code is present)</li>
	 * </ul>
	 * 
	 * @param roh The RulesObjectHolder containing all important invoice and rule data.
	 */
	public static void hospitalizationDay1And2Cumulation (RulesObjectsHolder roh)
	{
		Date		firstDay;
		Date		currentDay;
		Calendar	cal;
		int			cumulationMode;
		
		
		if (roh.getInvoice().isAmbulatory())
			// if the invoice is not for hospitalisation, skip it
			return;
		
		/* get the setting that defines, whether or not the acts of the first 2 days
		 * of an hospitalised invoice shall not be removed or reduced
		 */
		cumulationMode		= roh.getHospitalizedCumulationMode();
		
		if (cumulationMode == BillingAdminSettings.c_HospitalizedCumulationModeNormal)
			return;
		
		// set the options for those days, before the start of normal cumulation
		boolean cumulateFully;
		if (cumulationMode == BillingAdminSettings.c_HospitalizedCumulationModeFullWithFCode)
		{
			cumulateFully	= false;
			for (Act act : roh.getInvoice().getActs())
			{
				// is there any hospitalization fee in this invoice?
				if (RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_FORFAIT_HOSPITALISATION, act))
				{
					// hospitalization fee found
					cumulateFully	= true;
					break;
				}
			}
		}
		else
		{
			cumulateFully	= true;
		}
		
		if (!cumulateFully)
		{
			return;
		}
		
		List<List<Act>> sortedActs	= roh.getSortedActs();
		if (sortedActs.size() > 0 && sortedActs.get(0).size() > 0 && sortedActs.get(0).get(0) != null)
		{
			firstDay	= sortedActs.get(0).get(0).getPerformedDay();
		}
		else
		{
			// no acts available
			return;
		}
		
		// make of the first day of hospitalisation the first day of full cumulation period end
		cal		= new GregorianCalendar();
		cal.setTime(firstDay);
		cal.add(Calendar.DAY_OF_YEAR, FREE_HOSPITALISATION_CUMULATION_DAYS);
		firstDay	= cal.getTime();
		
		// mark all acts on the first 2 days, to be fully cumulated
		for (List<Act> actsOfDay : sortedActs)
		{
			if (actsOfDay.isEmpty())
			{
				continue;
			}
			
			currentDay	= actsOfDay.get(0).getSessionDate();
			if (!currentDay.before(firstDay))
			{
				// the other days are irrelevant
				break;
			}
			
			// this date is before the first day of normal cumulation
			roh.setDayRuleOption(currentDay, 
					RulesObjectsHolder.OPTION_PERFORM_ANY_CUMULATION, false);
			roh.setDayRuleOption(currentDay, 
					RulesObjectsHolder.OPTION_PERFORM_REJECTION, false);
			roh.setDayRuleOption(currentDay, 
					RulesObjectsHolder.OPTION_PERFORM_ELIMINATION, false);
			
			for (Act act : actsOfDay)
			{
				if (act.getQuantity().intValue() == 0)
					act.setQuantity(1);
				act.clearSuffix('R');
			}
		}
		
		roh.discardSortedActs();
		roh.getInvoice().monetize();
	}
	
	
	/**
	 * Check the minima for anesthetic and assistance acts 
	 * and apply it if necessary.
	 * 
	 * @param roh The RulesObjectHolder containing all important invoice and rule data.
	 */
	public static void checkMinima (RulesObjectsHolder roh)
	{
		boolean			changed			= false;
		List<Suffix>	minimaSuffixes	= RulesObjectsHolder.getSuffixWithMinimum();
		
		
		for (List<Act> actsOfDay : roh.getSortedActs())
		{
			for (Suffix s : minimaSuffixes)
				if (checkMinimaOfDay(actsOfDay, s)) 
					changed = true;
		}
		
		if (changed)
		{
			roh.discardSortedActs();
			roh.getInvoice().monetize();
		}
	}
	
	
	/**
	 * The special cumulation rules for the gynecologists (100% | 100% | 50% | 50% | 0%) are checked
	 * and applied if necessary.
	 * 
	 * @param roh The RulesObjectHolder containing all important invoice and rule data.
	 */
	public static void obstetricsCumulation (RulesObjectsHolder roh)
	{
		boolean		invoiceChanged		= false;
		List<Act>	obstetricsActs		= new LinkedList<Act>();
		List<Act>	actsToRemove		= new LinkedList<Act>();
		List<Act>	technicalActsOfDay	= new LinkedList<Act>();
		double		ruleAmount;
		double		defaultCumulationAmount;
		double		obstetricsCumulationAmount;
		int			noOfCumulatedActs;
		
		
		for (List<Act> actsOfDay : roh.getSortedActs())
		{
			if (!actsOfDay.isEmpty() && !roh.isDayRuleOption(
					actsOfDay.get(0).getSessionDate(), 
					RulesObjectsHolder.OPTION_PERFORM_OBSTETRICS_CUMULATION))
			{
				// if the option to perform any cumulation for this day is not set, skip this
				continue;
			}
			
			obstetricsActs.clear();
			actsToRemove.clear();
			technicalActsOfDay.clear();
			
			/* **************************************** */
			// 		Check the acts and their amounts 
			/* **************************************** */
			
			boolean sec_T_6_1_1_found = false;
			
			for (Act act : actsOfDay)
			{
				// the acts with the highest amount will come first
				
				if (act.getQuantity() == 0
						|| act.isRental() 
						|| act.isMaterial()
						|| RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_GEN_ACT, act.getCode())
						|| isCumulativeWithoutLimitation(act.getCode(), roh))
					// take only technical acts, that have a value and that aren't materials or rental fees
					continue;
				
				// add to technical acts
				technicalActsOfDay.add(act);
				if (RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_OBSTETRICAL_ACT, act.getCode()))
				{
					// add to obstetrical acts
					obstetricsActs.add(act);
				}
				
				if (RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_OBSTETRICAL_ACT_SOUS1, act.getCode())) {
					sec_T_6_1_1_found = true;
				}
					
			}
			
			if (! sec_T_6_1_1_found) return;
			
			// sort the technical acts and get their amount
			technicalActsOfDay			= ActSorter.sort(technicalActsOfDay, roh, 1.0, 0.5, 0.5);
			defaultCumulationAmount		= 0.0;
			StringBuffer sb = new StringBuffer("technicalActsOfDay\n");
			for (Act act : technicalActsOfDay)
			{
				ruleAmount	= act.getRuleValue().doubleValue();
				sb.append("\t" +act.getCode() + " " + ruleAmount + "\n");
				if (ruleAmount == 0.0)
					continue;
				else
					defaultCumulationAmount	+= ruleAmount;
			}
			System.out.println(sb.toString());
			
			// sort the obstetrical acts and get their amount
			obstetricsActs				= ActSorter.sort(obstetricsActs, roh, 1.0, 1.0, 0.5, 0.5);
			obstetricsCumulationAmount	= 0.0;
			sb = new StringBuffer("obstetricsActs\n");
			for (Act act : obstetricsActs)
			{
				ruleAmount	= act.getRuleValue().doubleValue();
				sb.append("\t" +act.getCode() + " " + ruleAmount + "\n");
				if (ruleAmount == 0.0)
					continue;
				else
					obstetricsCumulationAmount	+= ruleAmount;
			}
			System.out.println(sb.toString());
			
			if (defaultCumulationAmount >= obstetricsCumulationAmount)
				/* If the amount after applying the rules will be less than before
				 * the rule will not be applied for the current day
				 */
				continue;
			
			/* **************************************** */
			// 		APPLY THE RULE (100%|100%|50%|50%) 
			/* **************************************** */
			
			invoiceChanged		= true;
			roh.setDayRuleOption(obstetricsActs.get(0).getSessionDate(), 
					RulesObjectsHolder.OPTION_PERFORM_ANY_CUMULATION, false);
			
			// Apply the 100|100|50|50 rule, but check every rank, if there is an act
			noOfCumulatedActs	= 0;
			for (Act act : obstetricsActs)
			{
				if (noOfCumulatedActs < 2)
					cumulateAct(act, false);
				else if (noOfCumulatedActs < 4)
					cumulateAct(act, true);
				else
					removeAct(act);
				noOfCumulatedActs++;
			}
			
			int cumulatedActs = obstetricsActs.size();
			for (Act act : technicalActsOfDay)
			{
				if (!obstetricsActs.contains(act)) {
					// if we have used less than 3 obstetics acts, 
					// we can use additional technical acts as reduce rate 
					if (cumulatedActs < 3) {
						cumulateAct(act, true);
					// else we have to set the other technical acts to zero.
					} else {
						removeAct(act);											
					} 
					cumulatedActs++;
				} 
			}
		}
		
		if (invoiceChanged)
		{
			roh.discardSortedActs();
			roh.getInvoice().monetize();
		}
	}
	
	
	
	/* ======================================== */
	// 		HELP METHODS
	/* ======================================== */
	
	private static String debugPrintActs(String string, List<Act> acts) {
		StringBuffer sb = new StringBuffer("\n"+string+"\n");
		for (Act act : acts) {
			sb.append("\t").append(act).append("\n");
		}
		return sb.toString();
	}

	/**
	 * Check the minimum for the given suffix and the given acts 
	 * and apply it if necessary.
	 * 
	 * @param actsOfDay All acts of this session
	 * @param suffix The suffix to check the minimum for
	 * @return <code>true</code> if the minimum was found and applied, else <code>false</code>.
	 */
	private static boolean checkMinimaOfDay (List<Act> actsOfDay, Suffix suffix)
	{
		double		amount		= 0.0;
		boolean		minimumSet	= false;
		Act			firstAct;
		KeyValue	medicalKeyValue;
		double		minimum;
		
		
		firstAct		= actsOfDay.get(0);
		medicalKeyValue	= RulesObjectsHolder.getMedicalKeyValue(firstAct.getPerformedDate());
		minimum			= monetizeForMinimum(suffix.getMinimum(), medicalKeyValue);
		
		for (Act act : actsOfDay)
		{
			// add the amounts of all acts with the specified suffix
			if (act.suffixAlreadySet(suffix.getLetter().charValue()))
			{
				amount += act.calculateAmountForMinimum();
				if (amount >= minimum)
					// stop here
					return false;
			}
		}
		
		if (amount <= 0.0)
			return false;
		
		for (Act act : actsOfDay)
		{
			if (act.suffixAlreadySet(suffix.getLetter().charValue()))
			{
				// minimum applies, modify the one act with the given suffix and remove the rest
				if (!minimumSet) {
					// set the first act to the minimum ...
					act.setOrgCoefficient(act.getCoefficient());
					if (medicalKeyValue == null || medicalKeyValue.getValue().doubleValue() == act.getKeyValue().doubleValue())
					{
						act.setCoefficient(suffix.getMinimum());
					}
					else
					{
						// This act has doesn't have the medics key value, but a minimum requires always the medics key value
						// -> need to compensate the other key value:
						// as we don't want to change the acts keyvalue to medical, we modify the coefficient with 
						// a medicalkeyvalue/actkeyvalue factor instead to get the same result.  
						act.setCoefficient(suffix.getMinimum() * (medicalKeyValue.getValue().doubleValue() / act.getKeyValue().doubleValue()));
					}
//					act.setQuantity(1);
					act.monetize();
					minimumSet = true;
				} else {
					// ... and the other to 0
					removeAct(act);
					act.monetize();
				}
			}
		}
		
		return true;
	}
	
	
	private static double monetizeForMinimum (Double coefficient, KeyValue keyValue)
	{
		Act tmp = new Act();
		tmp.setCoefficient(coefficient);
		tmp.setKeyValue(keyValue.getValue(), keyValue.getFractionDigits());
		tmp.setQuantity(1);
		
		return tmp.monetize();
	}
	
	
	/**
	 * Adds or clears the suffix 'R' on the given act
	 * 
	 * @param act The act to change
	 * @param reduce Adds or clears the suffix 'R'
	 */
	private static void cumulateAct (Act act, boolean reduce)
	{
		act.setQuantity(1);
		
		if (reduce && !act.getCAT()) {
			act.setSuffix('R');			
		} else {
			act.clearSuffix('R');			
		}
		
		act.monetize();
	}
	
	
	/**
	 * Clears the suffix 'R' and sets the quantity of 
	 * given act to 0
	 * 
	 * @param act The act to remove
	 */
	public static void removeAct (Act act)
	{
		act.setQuantity(0);
		act.clearSuffix('R');
		act.monetize();
//		System.out.println("Removed Act " + act);
	}
	
	
	/**
	 * Remove all rental acts, that haven't their base act set
	 * 
	 * @param roh The RulesObjectHolder containing all important invoice and rule data.
	 */
	public static void checkRentalActs (RulesObjectsHolder roh)
	{
		Set<String>	baseXActs	= new HashSet<String>();
		List<Act>	xActs		= new LinkedList<Act>();
		String		code;
		boolean		changes		= false;
		
		
		for (List<Act> actsOfDay : roh.getSortedActs())
		{
			baseXActs.clear();
			xActs.clear();
			
			// put the acts into the correct list / map
			for (Act act : actsOfDay)
			{
				if (act.getQuantity().intValue() == 0)
					break;
				
				code	= act.getCode();
				
				if (code == null)
				{
					continue;
				}
				else if (code.endsWith(Act.c_RentalSuffix))
				{
					xActs.add(act);
					if (act.getSuffixes() != null
							&& act.getSuffixes().length() != 0)
					{
						act.clearAllSuffixes();
						act.monetize();
						changes = true;
					}
				}
				else if (act.getQuantity().intValue() > 0
						&& !act.suffixAlreadySet('A')
						&& !act.suffixAlreadySet('P'))
				{
					/* add only those acts, that haven't been removed and aren't 
					 * anesthetic (suffix 'A') or assistant (suffix 'P') acts
					 */
					baseXActs.add(code);
				}
			}
			
			// check the X-acts, whether there is a base act set
			for (Act xAct : xActs)
			{
				code		= xAct.getCode();
				code		= code.substring(0, code.length()-1);
				
				if (baseXActs.contains(code))
				{
					if (xAct.getQuantity().intValue() == 0)
					{
						// only set the quantity to 1, if it wasn't already greater than 0
						xAct.setQuantity(1);
						xAct.monetize();
						changes	= true;
					}
				}
				else if (xAct.getQuantity().intValue() > 0)
				{
					// only set the quantity to 0, if it wasn't 0
					removeAct(xAct);
					xAct.monetize();
					changes	= true;
				}
			}
		}
		
		if (changes)
			roh.discardSortedActs();
	}
	
	
	/**
	 * Removes either the general or technical acts (as far as not compatible with each other) from the invoice.
	 * 
	 * @param roh The RulesObjectHolder containing all important invoice and rule data.
	 */
	public static void rejectTechnicalOrGeneralMedicalActs (RulesObjectsHolder roh)
	{
		Map<Date, ActSorter>	actsOfDay	= roh.getActsOfSessions();
		List<Act>				generalActs;
		List<Act>				actsToKeep;
		List<Act>				actsToReset;
		String					code;
		Act						belongingAct;
		double					highestValue;
		double					cacsValue;
		double					currentValue;
		boolean					resetCACs;
		boolean					isCACCompatibleAct;
		boolean					technicalActFound;
		
		
		for (Date date : actsOfDay.keySet()) 
		{
			if (!roh.isDayRuleOption(date, RulesObjectsHolder.OPTION_PERFORM_REJECTION))
				// for this session, no rejections are necessary
				continue;
			
			generalActs			= new LinkedList<Act>();
			actsToKeep			= new LinkedList<Act>();
			actsToReset			= new LinkedList<Act>();
			highestValue		= 0.0;
			cacsValue			= 0.0;
			technicalActFound	= false;
			
			// sort all acts of this session into the according list(s)
			for (Act act : actsOfDay.get(date))
			{
				code	= act.getCode();
				
				if (isCumulativeWithoutLimitation(code, roh)
						|| act.isRental() 
						|| act.isMaterial())
					// Don't consider acts that can be cumulated without restriction
					// Rental and Material acts are handled together with their belonging act
					continue;
				
				if (RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_GEN_ACT, code))
				{
					// it's a general act
					if (RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_GEN_ACT_COMPATIBLE_WITH_TECH_ACTS, code))
					{
						// this is a kind of act, that is never reset
						continue;
					}
					
					generalActs.add(act);
				}
				else if (RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_TECH_ACT, code))
				{
					// it's a technical act
					technicalActFound	= true;
					
					if (act.getQuantity().intValue() == 0)
						// don't consider removed technical acts
						continue;
					
					actsToKeep.add(act);
					belongingAct	= roh.getBelongingActOf(act);
					highestValue	+= act.getValue();
					if (belongingAct != null)
						highestValue	+= belongingAct.getValue().doubleValue();
					
					if (act.getCAC() != null && act.getCAC().booleanValue())
					{
						// CACs have to be treated specially
						cacsValue	+= act.getValue().doubleValue();
						if (belongingAct != null)
							cacsValue	+= belongingAct.getValue().doubleValue();
					}
				}
			}
			
			if (!technicalActFound)
				continue;
			
			// check if there is any general act, that is (together with technical CAC acts) worth more, than all technical acts
			resetCACs	= false;
			for (Act act : generalActs)
			{
//				code			= act.getCode();
				// is it a CAC compatible act
				isCACCompatibleAct	= RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_MED_CAC_SUPPORTING_CONSULTAIONS, act);

				if (act.getQuantity().intValue() == 0)
				{
					// General acts that have been reset, could become interesting again
					// They are reset, if their not advantageous afterwards
					act.setQuantity(1);
					act.monetize();
				}
				
				currentValue		= act.getValue().doubleValue() + (isCACCompatibleAct ? cacsValue : 0);
				
				if (currentValue > highestValue)
				{
					// this act is worth more, than the last marked as "highest" act(s)
					highestValue	= currentValue;
					// mark the last marked as "highest" act(s), as "to be reset" ...
					actsToReset.addAll(actsToKeep);
					actsToKeep.clear();
					// ... mark the current act "highest"
					actsToKeep.add(act);
					// store whether or not, this act is compatible with CACs
					resetCACs	= !isCACCompatibleAct;
				}
				else
				{
					// this act isn't worth more, than the last marked act(s)
					actsToReset.add(act);
				}
			}
			
			for (Act act : actsToReset)
			{
				if (resetCACs || !act.getCAC().booleanValue())
					// if CAC acts should be reset or this isn't one, reset it. Otherwise, skip it
					removeAct(act);
			}
		}
	}
	
	
	private static void rejectTechnicalOrGeneralDentalActs (RulesObjectsHolder roh)
	{
		// TODO Implement the rejection of the dentist
	}
	
	
	public static void dentists3rdChapterCumulation (RulesObjectsHolder roh)
	{
		List<Act>				chapter3Acts	= new LinkedList<Act>();
		boolean					changed			= false;
		int						noOfCumulatedActs;
		
		
		for (List<Act> actsOfDay : roh.getSortedActs())
		{
			if (actsOfDay == null
					|| actsOfDay.isEmpty()
					|| !roh.isDayRuleOption(actsOfDay.get(0).getSessionDate(), 
							RulesObjectsHolder.OPTION_PERFORM_DENTAL_CUMULATION))
			{
				// no acts for this day or the option, to perform the default cumulation is not set for this day
				continue;
			}
			
			chapter3Acts.clear();
			for (Act act : actsOfDay)
			{
				// filter all technical acts of this day - without rental or material acts
				if (!act.isRental()
						&& !act.isMaterial()
						&& RulesObjectsHolder.indexIncludes(RulesObjectsHolder.PATTERN_DENT_TECH_CHAPTER_3_ACT, act)
						&& !isCumulativeWithoutLimitation(act.getCode(), roh))
				{
					// normal technical act found
					chapter3Acts.add(act);
				}
			}
			
			// sort the technical acts ...
			chapter3Acts		= ActSorter.sort(chapter3Acts, roh, 1.0, 0.5);
			// ... and cumulate them correctly
			noOfCumulatedActs	= 0;
			for (Act act : chapter3Acts)
			{
				if (noOfCumulatedActs < 1)
					cumulateAct(act, false);
				else if (noOfCumulatedActs < 2)
					cumulateAct(act, true);
				else
					removeAct(act);
				noOfCumulatedActs++;
			}
		}
		
		if (changed)
			// remove the sortedActs, as something was changed
			roh.discardSortedActs();
	}
	
	
	/**
	 * Fill the static maps that define if an act can be cumulated unlimited and which
	 * acts are treated as a group.<br>
	 * This is executed only once (at the first use of the rules).
	 */
	private static void initialize ()
	{
		// all acts of one group in a session are treated as one act during cumulation
		defineUnionCumulationActs(RulesObjectsHolder.getRateIndex("T_8_1"), false);	// nomenclature Art. 9 §4
		
		// these acts can be cumulated without limitation and without being reduced
		defineUnlimitedCumulativeActs(RulesObjectsHolder.getRateIndex("T_8_1_1"), false); // the material of radiology acts is unlimited
		defineUnlimitedCumulativeActs(RulesObjectsHolder.getRateIndex("T_7_2"), false);	// nomenclature Art. 10 9)
		defineUnlimitedCumulativeActs(RulesObjectsHolder.getRateIndex("G_1_5"), false);	// nomenclature Art. 10 9) & nomenclature Art. 12 §7
		
		// always regarded as own session
		defineUnlimitedCumulativeActs(RulesObjectsHolder.getRateIndex("T_7_4"), false);	// nomenclature Art. 12 §7
		defineUnlimitedCumulativeActs(RulesObjectsHolder.getRateIndex("G_4_6"), false);	// nomenclature Art. 12 §7
		defineUnlimitedCumulativeActs(RulesObjectsHolder.getRateIndex("G_4_7"), false);	// nomenclature Art. 12 §7
	}


	/**
	 * Adds all codes of a RateIndex as a group of union acts into the intended map, 
	 * so these codes will be valued as one code, when cumulating.
	 * 
	 * @param index The RateIndex, to take the codes from
	 * @param includeRentalAndMaterial Whether or not X- and M-codes should be included
	 * @param excludedCodes An array of codes, that should be excluded
	 */
	private static void defineUnionCumulationActs (RateIndex index, boolean includeRentalAndMaterial, String ... excludedCodes)
	{
		// load all codes of the given index
		List<String>		codes		= RulesObjectsHolder.getAllCodesOfRateIndex(index);
		Collection<String>	unionCodes	= new HashSet<String>();
		
		
		if (excludedCodes != null)
		{
			// exclude the code to be excluded
			for (String exclude : excludedCodes)
			{
				codes.remove(exclude);
			}
		}
		
		for (String code : codes)
		{
			if (includeRentalAndMaterial
					||(!code.toLowerCase().endsWith("x")
					&& !code.toLowerCase().endsWith("m")))
			{
				// if X- and M-codes are to be excluded, check this first
				unionCodes.add(code);
			}
		}
		
		defineUnionCumulationActs(unionCodes);
	}
	

	/**
	 * Adds all given codes as a group of union acts into the intended map, 
	 * so these codes will be valued as one code, when cumulating.
	 * 
	 * @param codes The codes to be add as group
	 */
	private static void defineUnionCumulationActs (Collection<String> codes)
	{
		for (String code : codes)
		{
			if (unionCumulationActs.put(code, codes) != null)
				// break, if there are duplicated codes
				throw new RuntimeException("Duplicate code \""+code+"\" in unionCumulationActs list.\n" +
						"A code may only appear in one list.");
		}
	}


	/**
	 * Defines all rates of this index and all sub indexes as to be 
	 * cumulated without reduction and limitation.
	 * 
	 * @param rateIndex The rate index to take all codes from
	 */
	private static void defineUnlimitedCumulativeActs (RateIndex rateIndex, boolean sessionModeOnly)
	{
		if (rateIndex == null)
			return;
		
		List<String> codes = RulesObjectsHolder.getAllCodesOfRateIndex(rateIndex);
		
		if (codes == null)
			return;
		
		if (sessionModeOnly)
			unlimitedSessionCumulativeActs.addAll(codes);
		else
			unlimitedCumulativeActs.addAll(codes);
	}
	
	
	private static boolean isCumulativeWithoutLimitation (String code, RulesObjectsHolder roh)
	{
		if (unlimitedCumulativeActs.contains(code))
			return true;
		else if (roh.isSessionMode() && unlimitedSessionCumulativeActs.contains(code))
			return true;
		else
			return false;
	}
}
