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

import java.text.DateFormat;
import java.text.SimpleDateFormat;
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 java.util.SortedSet;
import java.util.TreeMap;
import java.util.regex.Pattern;

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.Rate;
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.ejb.session.interfaces.NomenclatureInterface;
import lu.tudor.santec.gecamed.core.utils.GECAMedUtils;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
 * @author jens.ferring(at)tudor.lu
 * 
 * @version
 * <br>$Log: RulesObjectsHolder.java,v $
 * <br>Revision 1.7  2013-12-27 18:09:25  donak
 * <br>Cleanup of imports
 * <br>
 * <br>Revision 1.6  2013-07-15 06:18:36  ferring
 * <br>logging changed
 * <br>
 * <br>Revision 1.5  2013-04-23 12:27:05  ferring
 * <br>Billing rules fixed: 1. 2 days of hospitalisation
 * <br>
 * <br>Revision 1.4  2013-03-22 10:27:03  ferring
 * <br>UnionActs added and billing bugs fixed (since last not released commit)
 * <br>
 * <br>Revision 1.3  2013-03-14 10:40:20  ferring
 * <br>Billing rules corrected
 * <br>
 * <br>Revision 1.2  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>
 * <br>Revision 1.1  2013-02-27 08:04:29  ferring
 * <br>Rule system "outsourced" to helper classes, that can be converted to drools functions, if needed
 * <br>
 */

public class RulesObjectsHolder implements ActsChangedListener
{
	/* ======================================== */
	// 		CONSTANTS
	/* ======================================== */
	
	/* ---------------------------------------- */
	// INDEX PATTERN
	/* ---------------------------------------- */
	
	/* "med" or "medical" means: the nomenclature of the Médecins (livre bleu - part: "Médecins") concerning
	 * "dent" or "dental" means: the nomenclature of the Médecins-dentistes (livre bleu - part: "Médecins-dentistes") concerning
	 * "gen" or "general" means: the 1st part of the nomenclatures in the livre bleu, where all general acts are written down
	 * "tech" or "technical" means: the 2nd part of the nomenclatures in the livre bleu, where all technical acts are written down
	 */
	
	public static final String	PATTERN_INCLUDE_SUB_INDEXES						= "(_[1-9][0-9]?)*"; // to be added to include sub indexes
	
	// LIVRE BLEU
	public static final String	PATTERN_MED_AND_DENT_ACT						= "D?[GT]"			+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical & dental acts
	public static final String	PATTERN_MED_AND_DENT_GEN_ACT					= "D?G"				+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical & dental general acts
	public static final String	PATTERN_MED_AND_DENT_TECH_ACT					= "D?T"				+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical & dental technical acts
	
	// medical
	public static final String	PATTERN_MED_ACT									= "[GT]"			+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical acts
	public static final String	PATTERN_MED_GEN_ACT								= "G"				+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical general acts
	public static final String	PATTERN_MED_TECH_ACT							= "T"				+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical technical acts
	public static final String	PATTERN_MED_CAC_SUPPORTING_CONSULTAIONS			= "G_((1_[14])|6)"	+ PATTERN_INCLUDE_SUB_INDEXES;	// see nomenclature art. 10 1)
	public static final String	PATTERN_MED_GEN_ACT_COMPATIBLE_WITH_TECH_ACTS	= "G_((1_5)|(2_1)|5|3)"	+ PATTERN_INCLUDE_SUB_INDEXES;	// see nomenclature art. 10 2), 3)
	public static final String	PATTERN_MED_FORFAIT_HOSPITALISATION				= "G_4"				+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical F-codes
	public static final String	PATTERN_MED_ANESTHETIC_HOSPITALIZATION_FEES		= "G_4_[678]"		+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical F-codes of the anesthetics
	public static final String	PATTERN_MED_REPORT								= "G5"				+ PATTERN_INCLUDE_SUB_INDEXES;	// all medical reports
	
	public static final String	PATTERN_MED_OBSTETRICAL_ACT						= "T_6_1"			+ PATTERN_INCLUDE_SUB_INDEXES;	// all obstetrical acts (cumulated: 100%, 100%, 50%, 50%)
	public static final String	PATTERN_MED_OBSTETRICAL_ACT_SOUS1				= "T_6_1_1"			+ PATTERN_INCLUDE_SUB_INDEXES;	//
	public static final String	PATTERN_MED_OBSTETRICAL_ACT_SOUS2				= "T_6_1_2"			+ PATTERN_INCLUDE_SUB_INDEXES;	//
	public static final String	PATTERN_MED_OBSTETRICAL_ACT_SOUS3				= "T_6_1_3"			+ PATTERN_INCLUDE_SUB_INDEXES;	//

	// dental
	public static final String	PATTERN_DENT_ACT								= "D[GT]"			+ PATTERN_INCLUDE_SUB_INDEXES;	// all dental acts
	public static final String	PATTERN_DENT_TECH_CHAPTER_3_ACT					= "DT_3"			+ PATTERN_INCLUDE_SUB_INDEXES;	// all dental technical acts of the chapter 3
	
	public static final String	PATTERN_DONT_SHOW_TIME_INDEXES					= "G_4"				+ PATTERN_INCLUDE_SUB_INDEXES;	// all acts, whose time shouldn't be printed on the invoice
	
	
	/* ---------------------------------------- */
	// DAILY OPTIONS
	/* ---------------------------------------- */
	
	private static final int DEFAULT_OPTIONS							= -1;
	
	/**
	 * Defines whether the cumulations of Chapter /8.1 shall be performed
	 */
	public static final int OPTION_PERFORM_RADIODIAGNOSTIC_CUMULATION	= 1<<1;
	
	/**
	 * Defines whether the default cumulation (100% / 50% / 50% / 0% ...) 
	 * shall be performed
	 */
	public static final int	OPTION_PERFORM_DEFAULT_CUMULATION			= 1<<2;
	
	/**
	 * Defines whether the obstetrical cumulation (100|100|50|50) shall be performed
	 */
	public static final int	OPTION_PERFORM_OBSTETRICS_CUMULATION		= 1<<3;
	
	/**
	 * Defines whether the dental cumulation (100|50) shall be performed
	 */
	public static final int OPTION_PERFORM_DENTAL_CUMULATION			= 1<<4;
	
	/**
	 * Used to check all cumulation rules at once
	 */
	public static final int	OPTION_PERFORM_ANY_CUMULATION				= OPTION_PERFORM_DEFAULT_CUMULATION
																		| OPTION_PERFORM_OBSTETRICS_CUMULATION
																		| OPTION_PERFORM_RADIODIAGNOSTIC_CUMULATION
																		| OPTION_PERFORM_DENTAL_CUMULATION;
	/**
	 * Reject the least: general or technical acts
	 */
	public static final int OPTION_PERFORM_REJECTION					= 1<<5;
	
	/**
	 * Do not remove any of these acts.
	 */
	public static final int OPTION_PERFORM_ELIMINATION					= 1<<6;
	
	
	
	/* ======================================== */
	// 		MEMBERS
	/* ======================================== */
	
	
	/** the logger Object for this class */
	private static Logger logger = Logger.getLogger(RulesObjectsHolder.class.getName());

	private static transient boolean			debug				= false;
	
	private static HashMap<String, RateIndex>	rateIndexes			= new HashMap<String, RateIndex>();
	
	private static HashMap<String, String>		codeToIndexLabelMap	= new HashMap<String, String>();
	
	private static Map<String, Pattern>			compiledPatterns	= new HashMap<String, Pattern>();
	
	private static NomenclatureInterface		nomenclatureManager;
	
	
	private Invoice								invoice;
	
	private RuleOptions							ruleOptions;
	
	private List<List<Act>>						sortedActs;
	
	private transient TreeMap<Date, ActSorter>	actsOfSesson;
	
	private Collection<Act>						filteredActLists	= new LinkedList<Act>();
	
	// define the default settings
	private Map<Date, Integer>					dayRuleOptions		= new HashMap<Date, Integer>();
	
	private StringBuffer ruleLog = new StringBuffer();
	private DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
	
	
	
	/* ======================================== */
	// CONSTRUCTORS
	/* ======================================== */
	
	public RulesObjectsHolder (Invoice invoice, RuleOptions ruleOptions, NomenclatureInterface manager)
	{
		setNomenclatureInterface(manager);
		
		this.invoice		= invoice;
		this.ruleOptions	= ruleOptions;
		
		this.invoice.addActsChangedListener(this);
		initData();
	}
	
	
	private void initData ()
	{
		Set<String>		codes	= new HashSet<String>();
		List<Object[]>	codesToIndexLabels;
		
		
		// fetch the data to compare a code and its rate index label
		for (Act act : invoice.getActs())
			if (!codeToIndexLabelMap.containsKey(act.getCode()))
				codes.add(act.getCode());
		
		if (!codes.isEmpty())
		{
			codesToIndexLabels	= getNomenclatureInterface().getRateIndexLabelsForCodes(codes);
			
			synchronized (codeToIndexLabelMap)
			{
				for (Object[] codeToLabel : codesToIndexLabels)
					codeToIndexLabelMap.put((String) codeToLabel[0], (String) codeToLabel[1]);
			}
		}
		
		// set the majoration of the insurance for the invoice and all acts
		invoice.setAllMajorations(invoice.getHealthInsurance().getMajoration());
		invoice.monetize();
	}
	
	
	
	/* ======================================== */
	// 		CLASS BODY
	/* ======================================== */
	
	public Invoice getInvoice ()
	{
		return invoice;
	}
	
	
	/**
	 * @param option The option to check (RulesObjectsHolder.OPTION_*)
	 * @return <code>true</code> if the flag is set, else <code>false</code>
	 */
	public boolean isDayRuleOption (Date date, int option)
	{
		int	options;
		
		
//		date		= GECAMedUtils.stripTime(date);
		// by default, all options are set
		options	= dayRuleOptions.get(date) == null ? -1 : dayRuleOptions.get(date);
		
		return (options & option) == option;
	}
	
	
	/**
	 * @param option The option to change (RulesObjectsHolder.OPTION_*)
	 * @param flag <code>true</code> to set the flag or <code>false</code> to reset it
	 */
	public void setDayRuleOption (Date date, int option, boolean flag)
	{
		int	options;
		
		
//		date		= GECAMedUtils.stripTime(date);
		options	= dayRuleOptions.get(date) == null ? DEFAULT_OPTIONS : dayRuleOptions.get(date).intValue();
		
		if (flag)
		{
			// set a bit flag with an OR
			options	|= option;
		}
		else
		{
			// XOR -1 is the binary negation
			option	^= -1;
			// remove a bit flag with an AND with its negation
			options &= option;
		}
		
		dayRuleOptions.put(date, Integer.valueOf(options));
	}

	/**
	 * Returns the rank of specified act among acts performed the same day. Ranking
	 * of acts performed on same day is done by amount in descending order, i.e. 
	 * the most expensive act always ranks first.
	 * @param act specifies the act to get rank of.
	 * @return the rank of specified act among acts performed the same day.
	 */
	public Integer getRankOfAct (Act act)
		{
		return getRankOfAct (act, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE);
		}

	public Integer getRankOfAct (Act act, Boolean ignoreGerneralActs, Boolean ignoreMaterialActs, Boolean ignoreRentalActs, Boolean ignoreReports)
	{
		int rank = rankOfAct(act, ignoreGerneralActs, ignoreMaterialActs, ignoreRentalActs, ignoreReports);
		return rank;
	}
	
	
	public void filterAct (Act act)
	{
		filteredActLists.add(act);
	}
	
	
	public Collection<Act> unfilterActs ()
	{
		Collection<Act> filteredActs = new ArrayList<Act>(filteredActLists);
		filteredActLists.clear();
		
		return filteredActs;
	}
	
	
	public TreeMap<Date, ActSorter> getActsOfSessions ()
	{
		if (actsOfSesson == null)
			buildCumulationSupportStructures();
		
		return actsOfSesson;
	}
	
	
	public void setActsOfSessions (TreeMap<Date, ActSorter> actsOfSession)
	{
		this.actsOfSesson = actsOfSession;
	}
	
	
	public int getPatientAgeOnDayOfAct (Act act)
	{
		Calendar	actDate			= act.getPerformedCalendar();
		Calendar	patientBirthday	=  new GregorianCalendar();
		int			age;
		
		
		patientBirthday.setTime((Date) ruleOptions.getPatientsBirthday());
		age	= actDate.get(Calendar.YEAR) - patientBirthday.get(Calendar.YEAR);
		patientBirthday.add(Calendar.YEAR, age);
		
		if (actDate.before(patientBirthday))
			age--;
		
		return age;
	}
	
	
	public int getTiersPayantValue ()
	{
		return ruleOptions.getTiersPayantMinValue();
	}
	
	
	public boolean isSessionMode ()
	{
		return ruleOptions.isSessionMode();
	}
	
	
	public int getHospitalizedCumulationMode ()
	{
		return ruleOptions.getHospitalizedCumulationMode();
	}
	
	
	public void printActList ()
	{
		printActList(null, "");
	}
	
	
	public void printActList (String title, String desc)
	{
		printActList(getSortedActs(), title != null ? title : "Sorted Acts: ", desc);
	}
	
	
	public List<List<Act>> getSortedActs ()
	{
		if (sortedActs == null)
			this.sortedActs = resortActs();
		
		return this.sortedActs;
	}
	
	
	public void discardSortedActs ()
	{
		this.sortedActs	= null;
	}


	public Boolean containsBelongingActOf (Act p_Act)
	{
		return getBelongingActOf(p_Act) != null;
	}
	
	
	/**
	 * Searches in the same session for the rental (X-Code) or material (M-Code) of a non-rental and -material act
	 * or the normal act for a rental- or material-act.<br>
	 * Meaning, if e.g. act "1C11" is given, act "1C11X" will be returned, if it exists in the same session and vice versa. 
	 * 
	 * @param act The act to search the belonging act for 
	 * @return The belonging act or <code>null</code> if none was found in the same session.
	 */
	public Act getBelongingActOf (Act act)
	{
		String code = act.getCode();
		ActSorter actsOfDay;
		String suffix;
		
		
		if (act.getCode().endsWith(Act.c_MaterialSuffix))
			suffix = Act.c_MaterialSuffix;
		else if (act.getCode().endsWith(Act.c_RentalSuffix))
			suffix = Act.c_RentalSuffix;
		else
			suffix = Act.c_NoSuffix;
		//		throw new RuntimeException("Act must have one of the following suffixes: " + 
		//				Act.c_MaterialSuffix + Act.c_RentalSuffix);
		
		code = act.getCode();
		if (!suffix.equals(Act.c_NoSuffix))
			code = code.substring(0, (code.length() - suffix.length()));
		
		actsOfDay = getActsOfSessions().get(act.getSessionDate());
		
		if (actsOfDay == null)
			return null;
		
		for (Act comparedAct : actsOfDay.getData())
		{
			if ((suffix.equals(Act.c_NoSuffix)
					&& (comparedAct.getCode().equals(code + Act.c_MaterialSuffix)
					|| comparedAct.getCode().equals(code + Act.c_RentalSuffix)))
					|| (!suffix.equals(Act.c_NoSuffix)
					&& comparedAct.getCode().equals(code)))
			{
				if (comparedAct.getQuantity() > 0)
					return comparedAct;
			}
		}
		
		return null;
	}

	
	/**
	 * Sets the specified suffix for all acts on invoice whose rate are part of
	 * the specified UCM nomenclature chapter/section and being performed on the
	 * specified date.
	 * @param p_Suffix specifies the suffix to be set for matching acts
	 * @param p_Index specifies the UCM nomenclature chapter or section acts have
	 * to be part of to be eligible.
	 * @param p_DateOfInterest specifies the date that matching acts have to be
	 * performed on to be eligible.
	 */
	public void setSuffixForActsIn (Character p_Suffix, RateIndex p_Index, Date p_DateOfInterest)
	{
		Date dayOfInterest;
		Date dayOfAct;
		ActSorter actsOfDay;
		
		
		dayOfInterest = GECAMedUtils.stripTime(p_DateOfInterest);
		actsOfDay = getActsOfSessions().get(dayOfInterest);
		
		if (actsOfDay != null)
		{
			for (Act act : actsOfDay)
			{
				dayOfAct = GECAMedUtils.stripTime(act.getPerformedDate());
				
				if ((p_Index.includesAct(act)) && dayOfAct.equals(dayOfInterest))
				{
					act.setSuffix(p_Suffix);
				}
			}
		}
	}

	/**
	 * Returns the number of acts performed on specified date.
	 * @param dateOfInterest specifies the date to get the number of performed
	 * acts for.
	 * @return the number of acts performed on the specified day.
	 */
	public Integer getNumberOfActsForDate (Date dateOfInterest)
	{
		Date		day;
		ActSorter	acts;
		
		
		day 	= GECAMedUtils.stripTime (dateOfInterest);
		acts	= getActsOfSessions().get(day);
		
		if (acts == null)
			return Integer.valueOf(0);
		else
			return acts.size();
	}
	
	
	/**
	 * This is a debug method, that prints the given acts as a table into the log.
	 * 
	 * @param acts The acts to print
	 * @param title The headline for this print
	 */
	public void printActList (List<List<Act>> acts, String title, String desc)
	{
		
		StringBuilder	out	= new StringBuilder();
		
		out.append("\n"+title + " => " + desc);
		for (List<Act> list : acts)
		{
			out.append("\n   Acts performed ")
					.append(df.format(list.get(0).getPerformedCalendar().getTime()))
					.append("");
			
			for (Act a : list)
			{
				a.monetize();
				out.append("\n\t")
						.append(a.getQuantity().intValue() >= 100 ? "" : a.getQuantity().intValue() >= 10 ? " " : "  ")
						.append(a.getQuantity())
						.append("x ")
						.append(a.getCode());
				appendSpaces   (out, 6 - a.getCode().length());
				out		.append(a.getSuffixes());
				appendSpaces   (out, 5 - a.getSuffixes().length());
				out		.append(" = ")
						.append(a.getValue().doubleValue() >= 100 ? "" : a.getValue().doubleValue() >= 10 ? " " : "  ")
						.append(a.getValue());
				
				if (a instanceof UnionAct) {
					out.append(" UnionAct: ")
							.append(((UnionAct) a).printSummed())
							.append(" ");
				}
				
				if (!a.getAmount().equals(a.getValue())) {
					out.append(" (Amount: ")
							.append(a.getAmount())
							.append(" FixAmount: ")
							.append(a.getFixAmount())
							.append(")");
				}
			}
		}
		
		ruleLog.append(out).append("\n-------------------------------------------\n");
		
		if (!debug)
			return;
		
		logger.log(Level.INFO, out);
	}
	
	
	
	/* ======================================== */
	// STATIC METHODS
	/* ======================================== */
	
	/**
	 * Creates pattern out of regular expressions and caches them. 
	 * If a pattern is needed for the 2nd time it is loaded from
	 * the cache.
	 * 
	 * @param regex The regular expression to get the pattern from
	 * @return The pattern to the regular expression, either from cache or newly created
	 */
	public static Pattern getPattern (String regex)
	{
		Pattern pattern = compiledPatterns.get(regex);
		
		if (pattern == null)
		{
			pattern = Pattern.compile(regex);
			compiledPatterns.put(regex, pattern);
		}
		
		return pattern;
	}
	
	
	public static RateIndex getRateIndex (String indexName)
	{
		RateIndex index = rateIndexes.get(indexName);
		
		
		if (index == null)
		{
			try
			{
				index = nomenclatureManager.getRateIndexByLabel(indexName);
				if (index != null)
				{
					index	= nomenclatureManager.fetchLazyDependencies(index);
				}
				rateIndexes.put(indexName, index);
			}
			catch (Exception e)
			{
				
				logger.log(org.apache.log4j.Level.ERROR, e.getMessage(), e);
			}
		}
		
		return index;
	}
	
	
	public static Act updateAct (String rateName, Act act, boolean updateOnlyIfActExists)
	{
		Act updatedAct;
		
		
		if (act != null && rateName != null)
		{
			try
			{
				updatedAct = nomenclatureManager.initializeAct(rateName, act);
				if (updateOnlyIfActExists && updatedAct.getQuantity().intValue() > 0 && updatedAct.monetize() == 0.0)
				{
					// the act didn't exist at that time or doesn't exist anymore. 
					// -> return the original act
					return act;
				}
				
				updatedAct.setInvoiceId(act.getInvoiceId());
//				rates.put(rateName, rate);
			}
			catch (Exception e)
			{
				logger.log(Level.ERROR, e.getMessage(), e);
				updatedAct	= act;
			}
		}
		else 
		{
			updatedAct	= act;
		}
		
		return updatedAct;
	}
	
	
	public static Suffix getSuffix (char letter)
	{
		return Act.getAllSuffixes().get(Character.valueOf(letter));
	}
	
	
	public static List<Suffix> getSuffixWithMinimum ()
	{
		List<Suffix> list	= new LinkedList<Suffix>();
		
		for (Suffix s : Act.getAllSuffixes().values())
		{
			if (s.getMinimum() > 0.0)
				list.add(s);
		}
		
		return list;
	}
	
	
	/**
	 * @param index The index to get all codes of
	 * @return The codes contained in the given index and all its sub indexes
	 */
	public static List<String> getAllCodesOfRateIndex (RateIndex index)
	{
		List<String>	codes	= new LinkedList<String>();
		
		if (index == null)
			return codes;
		
		for (Rate rate : index.getRates())
		{
			codes.add(rate.getCode());
		}
		
		for (RateIndex subIndex : index.getSubChapters())
		{
			codes.addAll(getAllCodesOfRateIndex(subIndex));
		}
		
		return codes;
	}
	
	
	/**
	 * @param code The code of a rate index to retrieve the label of
	 * @return The label and unique definer of the rate index, which contains the rate with the given code.
	 */
	public static String getIndexLabelOfCode (String code)
	{
		String indexLabel	= codeToIndexLabelMap.get(code);
		
		if (indexLabel == null)
		{
			indexLabel	= getNomenclatureInterface().getRateIndexLabelForCode(code);
			
			if (indexLabel != null)
			{
				synchronized (codeToIndexLabelMap)
				{
					codeToIndexLabelMap.put(code, indexLabel);
				}
			}
		}
		
		return indexLabel;
	}
	
	
	public static void putRateIndex (String label, RateIndex index)
	{
		rateIndexes.put(label, index);
	}
	
	
	public static void clearAll ()
	{
//		rates.clear();
		rateIndexes.clear();
		codeToIndexLabelMap.clear();
	}
	
	
	public static void setNomenclatureInterface (NomenclatureInterface nomenclatureInterface) {
		nomenclatureManager = nomenclatureInterface;
	}
	
	
	public static NomenclatureInterface getNomenclatureInterface () {
		return nomenclatureManager;
	}
	

	public static boolean indexIncludes (String indexLabel, Act act)
	{
		return indexIncludes(indexLabel, act.getCode());
	}
	
	
	public static boolean indexIncludes (String indexPattern, String code)
	{
		String	codeIndexLabel	= getIndexLabelOfCode(code);
		Pattern	pattern			= getPattern(indexPattern);
		
		return pattern.matcher(codeIndexLabel).matches();
	}


	public static KeyValue getMedicalKeyValue (Date performedDate)
	{
		KeyValue keyValue;
		
		
		try
		{
			keyValue = nomenclatureManager.getKeyValueByTypeAndPerformedDate(KeyValue.TYPE_MEDICS, performedDate);
		}
		catch (Exception e)
		{
			logger.error("Couldn't find lettre clé of CNS Médecins under the key type " + KeyValue.TYPE_MEDICS, e);
			return null;
		}
		
		return keyValue;
	}


	public static String[] generateCodeArray (Object ... definitions)
	{
		List<String>	codes	= new LinkedList<String>();
		
		
		for (Object definition : definitions)
		{
			if (definition instanceof String)
				codes.add((String) definition);
			else if (definition instanceof RateIndex)
				codes.addAll(getAllCodesOfRateIndex((RateIndex) definition));
		}
		
		return codes.toArray(new String[codes.size()]);
	}
	
	
	public static void enableDebugMode (boolean flag)
	{
		debug	= flag;
	}


	public static void toggleDebugMode ()
	{
		debug = !debug;
	}
	
	
	
	/* ======================================== */
	// HELP METHODS
	/* ======================================== */
	
	/**
	 * the buildCumulationSupportStructures method builds a couple of lookup tables
	 * required by the helper methods required for cumulation rules. Helper methods
	 * will call this method if structures are found to be non existing.
	 */
	private void buildCumulationSupportStructures ()
	{
//		TreeMap<Date, ActSorter>	actsOfDays	= getActsOfSessions();
		Date		performedTime;
		ActSorter	actsOfDay;
		
		
		// the date of an act won't change during the rules. Do this only once
		if (actsOfSesson == null)
		{
			actsOfSesson	= new TreeMap<Date, ActSorter>();
			for (Act act : invoice.getActs())
			{
				performedTime	= act.getSessionDate();
				actsOfDay		= actsOfSesson.get(performedTime);
				if (actsOfDay == null)
				{
					actsOfDay	= new ActSorter();
					actsOfSesson.put(performedTime, actsOfDay);
				}
				actsOfDay.add(act);
			}
		}
	}
	
	
	private List<List<Act>> resortActs ()
	{
		List<List<Act>> sortedActs;
		
		
		sortedActs	= new ArrayList<List<Act>>(getActsOfSessions().size());
		
		for (ActSorter acts : getActsOfSessions().values())
			sortedActs.add(new ArrayList<Act>(acts));
		
		return sortedActs;
	}
	
	
	/**
	 * <p>In opposite to calculateReducedRankValue, the full rank value doesn't take into 
	 * account things that would get lost, if the act was removed.</p>
	 * <p>The result of this method is used to define, if the act should be reduced or not.</p>
	 * 
	 * @param act The act the value is to be calculated
	 * @return The value of the act needed to compare
	 */
	public double calculateFullRankValue (Act act)
	{
		double fullRateValue;
		
		
		if (act.suffixAlreadySet('R'))
		{
			act.clearSuffix('R');
			fullRateValue	= act.monetize();
			act.setSuffix('R');
			act.monetize();
		}
		else
		{
			fullRateValue	= act.monetize();
		}
		
		return fullRateValue;
	}
	
	
	/**
	 * <p>In opposite to calculateFullRankValue, the reduced rank value takes all those things 
	 * into account, that would be lost, if the act was removed completely.<br>
	 * <i>The reduction is set to 50%.</i></p>
	 * <p>The result of this method is used to define, if the act should be removed or reduced.</p>
	 * 
	 * 
	 * @param act The act the value is to be calculated
	 * @return The reduced value of the act + all values that would be removed, if the act was removed.
	 */
	public double calculateReducedRankValue (Act act)
	{
		return calculateReducedRankValue(act, 0.5);
	}
	
	
	/**
	 * <p>In opposite to calculateFullRankValue, the reduced rank value takes all those things 
	 * into account, that would be lost, if the act was removed completely.</p>
	 * <p>The result of this method is used to define, if the act should be removed or reduced.</p>
	 * 
	 * @param act The act the value is to be calculated
	 * @param reduction The value the act will be reduced, if it's gonna be reduced (value * reduction).
	 * @return The reduced value of the act + all values that would be removed, if the act was removed.
	 */
	public double calculateReducedRankValue (Act act, double reduction)
	{
		double	reducedRankValue = act.monetize();
		Act		belongingAct;
		
		
		if (act.isMaterial()
				|| act.isRental())
			return 0.0;
		
		// check CAT and reduction
		if (!act.getCAT().booleanValue() && !act.suffixAlreadySet('R'))
			reducedRankValue *= reduction;
		
		// check for belonging M and X acts
		belongingAct	= getBelongingActOf(act);
		if (belongingAct != null)
		{
			reducedRankValue	+= belongingAct.getValue().doubleValue();
		}
		
//		// check CAC
//		if (act.getCAC().booleanValue())
//		{
//			ActSorter	actsOfDay = invoice.getActsOfSession().get(act.getSessionDate());
//			String		anotherCode;
//			
//			if (actsOfDay != null)
//			{
//				for (Act anotherAct : actsOfDay)
//				{
//					anotherCode	= anotherAct.getCode();
//					
//					if (anotherCode.equals(act.getCode()))
//					{
//						continue;
//					}
//					else if (Cumulations.PATTERN_CAC_SUPPORTING_CONSULTAIONS
//						.matcher(RulesObjectsHolder.getIndexLabelOfCode(anotherCode)).matches())
//					{
//						// add the CAC compatible act value, if it's a CAC act
//						reducedRankValue	+= anotherAct.getValue().doubleValue();
//					}
//				}
//			}
//		}
		
		return reducedRankValue;
	}
	
	
	private Integer rankOfAct (Act act, Boolean ignoreGerneralActs, Boolean ignoreMaterialActs, Boolean ignoreRentalActs, Boolean ignoreReports)
	{
		ActSorter	actsOfDay;
		Date		dayOfInterest;
		int			rank		= 0;
		
		
		if (act == null
				|| act.getQuantity().intValue() == 0) 
			return 0;
		
		dayOfInterest = act.getPerformedDay();
		
		// ... and just load it, instead of resorting every time
		actsOfDay		= getActsOfSessions().get(dayOfInterest);
		
		if (	   (ignoreMaterialActs 	&& act.isMaterial())
				|| (ignoreRentalActs 	&& act.isRental())
				|| (ignoreGerneralActs 	&& indexIncludes(PATTERN_MED_GEN_ACT, act.getCode()))
				|| (ignoreReports 		&& indexIncludes(PATTERN_MED_REPORT, act.getCode())))
			return 0;
		
		for (Act comparedAct : actsOfDay)
		{
			if (act.equals(comparedAct))
				// first increase the rank, than return it
				return ++rank;
			
			if (!(		(ignoreMaterialActs && comparedAct.isMaterial())
					|| 	(ignoreRentalActs 	&& comparedAct.isRental())
					|| 	(ignoreGerneralActs && indexIncludes(PATTERN_MED_GEN_ACT, comparedAct.getCode()))
					|| 	(ignoreReports 		&& indexIncludes(PATTERN_MED_REPORT, comparedAct.getCode()))))
			{
				rank++;
			}
		}
		
		return 0;
	}


	public int getLengthOfStay ()
	{
		
		if (invoice.getHospitalisation() != null
				&& invoice.getHospitalisation().getLengthOfStay() != null)
		{
			// there is a HL7 hospitalisation with the needed infos
			return invoice.getHospitalisation().getLengthOfStay().intValue();
		}
		else if (!isSessionMode())
		{
			// session mode is off, the acts are split by day
			return getActsOfSessions().values().size();
		}
		else
		{
			// check the grouping of the acts and count if the day changes
			int			days		= 0;
			Date		lastDay		= null;
			Set<Date>	sessions	= getActsOfSessions().keySet();
			
			
			if (!(sessions instanceof SortedSet))
				sessions	= new HashSet<Date>(sessions);
			
			for (Date date : sessions)
			{
				date = GECAMedUtils.stripTime(date);
				
				if (lastDay == null
						|| lastDay.before(date))
				{
					days++;
					lastDay	= date;
				}
			}
			
			return days;
		}
	}


	public boolean includesAct (String code)
	{
		if (GECAMedUtils.isEmpty(code))
			return false;
		
		for (Act act : invoice.getActs())
			if (code.equals(act.getCode()))
				return true;
				
		return false;
	}

	public void actsChanged (Invoice invoice)
	{
		actsOfSesson	= null;
	}
	
	
	private static void appendSpaces (StringBuilder out, int noOfSpaces)
	{
		for (int i = 0; i < noOfSpaces; i++)
			out.append(" ");
	}
	
	public void appendLog(String log) {
		ruleLog.append("\n").append(log);
	}
	
	public String getRuleLog() {
		return ruleLog.toString();
	}
	
	public static boolean isDebug() {
		return debug;
	}
}
