/*******************************************************************************
 *                                                                              
 *  Copyright FUJITSU LIMITED 2017
 *                                                                              
 *  Creation Date: Dec 10, 2012                                                      
 *                                                                              
 *******************************************************************************/

package org.oscm.billingservice.business.calculation.revenue;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.oscm.billingservice.business.calculation.BigDecimals;
import org.oscm.billingservice.business.calculation.revenue.model.TimeSlice;
import org.oscm.billingservice.business.calculation.revenue.model.UsageDetails;
import org.oscm.billingservice.business.calculation.revenue.model.UserAssignment;
import org.oscm.billingservice.business.calculation.revenue.model.UserAssignmentDetails;
import org.oscm.billingservice.business.calculation.revenue.model.UserAssignmentFactors;
import org.oscm.billingservice.business.calculation.revenue.model.UserRoleAssignment;
import org.oscm.billingservice.dao.BillingDataRetrievalServiceLocal;
import org.oscm.billingservice.dao.model.RolePricingData;
import org.oscm.billingservice.dao.model.RolePricingDetails;
import org.oscm.billingservice.dao.model.XParameterData;
import org.oscm.billingservice.dao.model.XParameterIdData;
import org.oscm.billingservice.dao.model.XParameterPeriodValue;
import org.oscm.billingservice.service.model.BillingInput;
import org.oscm.domobjects.ParameterHistory;
import org.oscm.domobjects.PriceModelHistory;
import org.oscm.domobjects.SubscriptionHistory;
import org.oscm.domobjects.UsageLicenseHistory;
import org.oscm.domobjects.enums.ModificationType;
import org.oscm.types.exceptions.BillingRunFailed;
import org.oscm.internal.types.enumtypes.PricingPeriod;
import org.oscm.internal.types.exception.IllegalArgumentException;

public class CostCalculatorPerUnit extends CostCalculator {

    CostCalculatorPerUnit() {
        super();
    }

    @Override
    public double computeFactorForUsageTime(PricingPeriod pricingPeriod,
            BillingInput billingInput, long usagePeriodStart,
            long usagePeriodEnd) {
        return computeFactor(pricingPeriod, billingInput, usagePeriodStart,
                usagePeriodEnd, true, true);
    }

    /**
     * Calculate the factor for a specific billing period and usage period.
     * According to the flag of extend usage start/end, the usage period time is
     * extended (or not) to the start/end time of the time unit before the
     * factor is calculated.
     */
    double computeFactor(PricingPeriod pricingPeriod,
            BillingInput billingInput, long usagePeriodStart,
            long usagePeriodEnd, boolean adjustsPeriodStart,
            boolean adjustsPeriodEnd) {

        if (usagePeriodEnd < usagePeriodStart) {
            throw new IllegalArgumentException("Usage period end ("
                    + new Date(usagePeriodEnd)
                    + ") before usage period start ("
                    + new Date(usagePeriodStart) + ")");
        }

        Calendar adjustedBillingPeriodStart = PricingPeriodDateConverter
                .getStartTime(billingInput.getCutOffDate(), pricingPeriod);
        Calendar adjustedBillingPeriodEnd = PricingPeriodDateConverter
                .getStartTime(billingInput.getBillingPeriodEnd(), pricingPeriod);

        if (usagePeriodOutsideOfAdjustedBillingPeriod(usagePeriodStart,
                usagePeriodEnd, adjustedBillingPeriodStart.getTimeInMillis(),
                adjustedBillingPeriodEnd.getTimeInMillis())) {
            return 0D;
        } else {
            Calendar startTimeForFactorCalculation = determineStartTimeForFactorCalculation(
                    pricingPeriod, adjustedBillingPeriodStart,
                    usagePeriodStart, adjustsPeriodStart);
            Calendar endTimeForFactorCalculation = determineEndTimeForFactorCalculation(
                    pricingPeriod, adjustedBillingPeriodEnd, usagePeriodEnd,
                    adjustsPeriodEnd);

            return computeFractionalFactor(
                    startTimeForFactorCalculation.getTimeInMillis(),
                    endTimeForFactorCalculation.getTimeInMillis(),
                    pricingPeriod);
        }
    }

    /**
     * Check if the usage period for factor calculation is located outside of
     * the billing period, which was adjusted to an overlapping time unit. In
     * this case no billing is done because the overlapping time unit is charged
     * in another billing period.
     */
    private boolean usagePeriodOutsideOfAdjustedBillingPeriod(
            long usagePeriodStart, long usagePeriodEnd,
            long adjustedBillingPeriodStart, long adjustedBillingPeriodEnd) {
        return (usagePeriodStart > adjustedBillingPeriodEnd || usagePeriodEnd < adjustedBillingPeriodStart);
    }

    /**
     * Calculate the factor for a specific time slice and usage period.
     * According to the flag of extend usage start/end, the usage period time is
     * extended (or not) to the start/end time of the time unit before the
     * factor is calculated.
     */
    double computeFactorForTimeSlice(TimeSlice timeSlice,
            long usagePeriodStart, long usagePeriodEnd,
            boolean adjustsPeriodStart, boolean adjustsPeriodEnd) {

        if (usagePeriodEnd < usagePeriodStart) {
            throw new IllegalArgumentException("Usage period end ("
                    + new Date(usagePeriodEnd)
                    + ") before usage period start ("
                    + new Date(usagePeriodStart) + ")");
        }

        Calendar startTimeForFactorCalculation = determineStartTimeForFactorCalculation(
                timeSlice.getPeriod(), timeSlice.getStartAsCalendar(),
                usagePeriodStart, adjustsPeriodStart);
        Calendar endTimeForFactorCalculation = determineEndTimeForFactorCalculation(
                timeSlice.getPeriod(),
                timeSlice.getStartOfNextSliceAsCalendar(), usagePeriodEnd,
                adjustsPeriodEnd);

        if (startTimeForFactorCalculation.after(endTimeForFactorCalculation)) {
            throw new BillingRunFailed("Usage period start ("
                    + new Date(usagePeriodStart) + ") and usage period end ("
                    + new Date(usagePeriodEnd) + ") do not match time slice ("
                    + timeSlice + ")");
        } else {
            return computeFractionalFactorForTimeUnit(
                    startTimeForFactorCalculation.getTimeInMillis(),
                    endTimeForFactorCalculation.getTimeInMillis(),
                    timeSlice.getPeriod());
        }
    }

    private Calendar determineStartTimeForFactorCalculation(
            PricingPeriod pricingPeriod, Calendar chargedPeriodStart,
            long usagePeriodStart, boolean adjustUsagePeriodStart) {
        Calendar startTime;
        if (adjustUsagePeriodStart) {
            startTime = PricingPeriodDateConverter.getStartTime(
                    usagePeriodStart, pricingPeriod);
        } else {
            startTime = Calendar.getInstance();
            startTime.setTimeInMillis(usagePeriodStart);
        }

        if (startTime.before(chargedPeriodStart)) {
            return chargedPeriodStart;
        } else {
            return startTime;
        }
    }

    private Calendar determineEndTimeForFactorCalculation(
            PricingPeriod pricingPeriod, Calendar chargedPeriodEnd,
            long usagePeriodEnd, boolean adjustUsagePeriodEnd) {
        Calendar endTime;
        if (adjustUsagePeriodEnd) {
            endTime = PricingPeriodDateConverter.getStartTimeOfNextPeriod(
                    usagePeriodEnd, pricingPeriod);
        } else {
            endTime = Calendar.getInstance();
            endTime.setTimeInMillis(usagePeriodEnd);
        }

        if (endTime.after(chargedPeriodEnd)) {
            return chargedPeriodEnd;
        } else {
            return endTime;
        }
    }

    @Override
    public long computeUserAssignmentStartTimeForParameters(
            PricingPeriod period, long paramValueEndTime,
            ParameterHistory paramHist, PriceModelHistory pmh,
            long paramValueStartTime) {
        long userAssignmentValueStartTime = paramValueStartTime;
        if (paramHist.getModtype().equals(ModificationType.MODIFY)) {
            Calendar adjustedUserAssignmentValueStartTime = PricingPeriodDateConverter
                    .getStartTimeOfNextPeriod(paramValueStartTime, period);
            if (adjustedUserAssignmentValueStartTime.getTimeInMillis() <= paramValueEndTime) {
                userAssignmentValueStartTime = adjustedUserAssignmentValueStartTime
                        .getTimeInMillis();
            }
        }
        return userAssignmentValueStartTime;
    }

    @Override
    public UserAssignmentFactors computeUserAssignmentsFactors(
            List<UsageLicenseHistory> ulHistList,
            PriceModelHistory referencePMHistory, BillingInput billingInput,
            long periodStart, long periodEnd) {

        final PricingPeriod period = referencePMHistory.getPeriod();
        final UserAssignmentFactors result = new UserAssignmentFactors();
        if (ulHistList != null && !ulHistList.isEmpty()) {
            UserAssignmentExtractor uaExtractor = new UserAssignmentExtractor(
                    ulHistList, periodStart, periodEnd);
            uaExtractor.extract();
            for (Long userKey : uaExtractor.getUserKeys()) {
                computeUserFactorAndRoleFactorForOneUser(result,
                        uaExtractor.getUserAssignments(userKey), period,
                        billingInput);
            }
        }
        return result;
    }

    /**
     * Compute user factor and role factor from user assignments for one user.
     * Iterate all assignments from newest one to oldest one. When it found that
     * two neighboring assignments belongs to different time slices, it's
     * considered as those two assignments have to be calculated separately and
     * calculate factor to previous ones. In the end, the factor for current
     * assignment and ones considerable as connected to the current one are
     * computed.
     */
    void computeUserFactorAndRoleFactorForOneUser(UserAssignmentFactors result,
            final List<UserAssignment> userAssignmentsForOneUser,
            PricingPeriod period, BillingInput billingInput) {

        UserAssignment previousAssignment = null;
        List<UserAssignment> userAssignmentsForSameSlices = new ArrayList<UserAssignment>();

        if (!userAssignmentsForOneUser.isEmpty()) {
            long userKey = userAssignmentsForOneUser.get(0).getUserKey();
            String userId = userAssignmentsForOneUser.get(0).getUserId();
            for (UserAssignment userAssignment : userAssignmentsForOneUser) {
                if (previousAssignment != null
                        && areNotAffectingSameTimeSlice(userAssignment,
                                previousAssignment, period)) {
                    computeUserFactorForAssignmentsAffectingSameSlices(result,
                            userAssignmentsForSameSlices, period, billingInput,
                            userKey, userId);
                    computeUserRoleFactorForAssignmentsAffectingSameSlices(
                            result, userAssignmentsForSameSlices, period,
                            billingInput, userKey);
                    userAssignmentsForSameSlices.clear();
                }

                userAssignmentsForSameSlices.add(userAssignment);
                previousAssignment = userAssignment;
            }

            computeUserFactorForAssignmentsAffectingSameSlices(result,
                    userAssignmentsForSameSlices, period, billingInput,
                    userKey, userId);
            computeUserRoleFactorForAssignmentsAffectingSameSlices(result,
                    userAssignmentsForSameSlices, period, billingInput, userKey);
        }
    }

    private boolean areNotAffectingSameTimeSlice(
            UserAssignment currentAssignment,
            UserAssignment previousAssignment, PricingPeriod period) {
        long nextSliceStartForCurrentEndTime = PricingPeriodDateConverter
                .getStartTimeOfNextPeriod(currentAssignment.getUsageEndTime(),
                        period).getTimeInMillis();
        long sliceStartForPreviousStartTime = PricingPeriodDateConverter
                .getStartTime(previousAssignment.getUsageStartTime(), period)
                .getTimeInMillis();
        if (nextSliceStartForCurrentEndTime <= sliceStartForPreviousStartTime) {
            return true;
        }
        return false;
    }

    private void computeUserFactorForAssignmentsAffectingSameSlices(
            UserAssignmentFactors result, List<UserAssignment> userAssignments,
            PricingPeriod period, BillingInput billingInput, long userKey,
            String userId) {

        long startTime = userAssignments.get(userAssignments.size() - 1)
                .getUsageStartTime();
        long endTime = userAssignments.get(0).getUsageEndTime();

        double factor = computeFactor(period, billingInput, startTime, endTime,
                true, true);
        storeUserFactorToResult(result, factor, userKey, userId);
    }

    private void storeUserFactorToResult(UserAssignmentFactors result,
            double factor, long userKey, String userId) {
        if (factor != 0) {
            UsageDetails usageDetails = new UsageDetails();
            usageDetails.setFactor(factor);
            result.addUsageDataForUser(Long.valueOf(userKey), userId,
                    usageDetails);
        }
    }

    private void computeUserRoleFactorForAssignmentsAffectingSameSlices(
            UserAssignmentFactors result,
            final List<UserAssignment> userAssignments, PricingPeriod period,
            BillingInput billingInput, long userKey) {

        List<UserRoleAssignment> extendedRoleAssignments = determineRoleAssignmentsWithFillingBlank(userAssignments);
        for (UserRoleAssignment roleAssignment : extendedRoleAssignments) {
            double factor = computeFactor(period, billingInput,
                    roleAssignment.getStartTime(), roleAssignment.getEndTime(),
                    roleAssignment.isFirstRoleAssignment(),
                    roleAssignment.isLastRoleAssignment());
            if (factor != 0) {
                storeUserRoleFactorToResult(result, roleAssignment, factor,
                        userKey);
            }
        }
    }

    /**
     * Create list of arranged user role assignments so that they can be
     * calculated with filling blanks which are between user assignments
     * affecting one same time slice. The end time of user role assignment is
     * extended to the start time of one newer user assignment, if the last user
     * role of older user assignment is different from the first role of newer
     * user assignment.
     */
    List<UserRoleAssignment> determineRoleAssignmentsWithFillingBlank(
            List<UserAssignment> userAssignmentsForOneUser) {
        List<UserRoleAssignment> determinedRoleAssignments = new ArrayList<UserRoleAssignment>();
        if (!userAssignmentsForOneUser.isEmpty()
                && userAssignmentsForOneUser.get(0).hasUserRole()) {
            UserRoleAssignment previousRoleAssignment = null;
            long endTime = 0;
            for (UserAssignment userAssignment : userAssignmentsForOneUser) {
                for (UserRoleAssignment roleAssignment : userAssignment
                        .getRoleAssignments()) {
                    if (previousRoleAssignment == null) {
                        endTime = roleAssignment.getEndTime();
                    } else if (!roleAssignment.getRoleKey().equals(
                            previousRoleAssignment.getRoleKey())) {
                        determinedRoleAssignments
                                .add(new UserRoleAssignment(
                                        previousRoleAssignment.getRoleKey(),
                                        previousRoleAssignment.getStartTime(),
                                        endTime));
                        endTime = previousRoleAssignment.getStartTime();
                    }

                    previousRoleAssignment = roleAssignment;
                }
            }

            if (previousRoleAssignment != null) {
                determinedRoleAssignments.add(new UserRoleAssignment(
                        previousRoleAssignment.getRoleKey(),
                        previousRoleAssignment.getStartTime(), endTime));
            }

            setFlagForFirstAndLastRole(determinedRoleAssignments);
        }
        return determinedRoleAssignments;
    }

    private void setFlagForFirstAndLastRole(
            List<UserRoleAssignment> determinedRoleAssignments) {
        if (!determinedRoleAssignments.isEmpty()) {
            determinedRoleAssignments.get(0).setLastRoleAssignment(true);
            determinedRoleAssignments.get(determinedRoleAssignments.size() - 1)
                    .setFirstRoleAssignment(true);
        }
    }

    private void storeUserRoleFactorToResult(UserAssignmentFactors result,
            UserRoleAssignment roleAssignment, double factor, long userKey) {
        UserAssignmentDetails detail = result.getUserAssignmentDetails(Long
                .valueOf(userKey));
        if (detail != null) {
            detail.addRoleFactor(roleAssignment.getRoleKey(), factor);
        }
    }

    @Override
    public long determineStartTime(long startTimeForPeriod,
            long endTimeForPeriod, PricingPeriod period) {
        if (startTimeForPeriod == endTimeForPeriod) {
            return startTimeForPeriod;
        }
        return PricingPeriodDateConverter.getStartTime(startTimeForPeriod,
                period).getTimeInMillis();
    }

    @Override
    public long computeEndTimeForPaymentPreview(long endTimeForPeriod,
            long billingPeriodEnd, PricingPeriod period) {
        long endTimeForPaymentPreview = PricingPeriodDateConverter
                .getStartTimeOfNextPeriod(endTimeForPeriod - 1, period)
                .getTimeInMillis();
        if (endTimeForPaymentPreview > billingPeriodEnd) {
            endTimeForPaymentPreview = billingPeriodEnd;
        }
        return endTimeForPaymentPreview;
    }

    @Override
    public void computeParameterPeriodFactor(BillingInput billingInput,
            XParameterData parameterData, long startTimeForPeriod,
            long endTimeForPeriod) {

        if (subscriptionHasParameters(parameterData)) {
            List<XParameterIdData> notChargedParameters = new ArrayList<XParameterIdData>();
            Map<XParameterIdData, Set<XParameterPeriodValue>> notChargedParameterValues = new LinkedHashMap<XParameterIdData, Set<XParameterPeriodValue>>();

            for (XParameterIdData parameterIdData : parameterData.getIdData()) {
                resetParameterPeriodFactorsToZero(parameterIdData);
                Map<TimeSlice, LinkedList<XParameterPeriodValue>> valuesPerSlice = mapParameterValuesToTimeSlices(
                        billingInput, parameterData, startTimeForPeriod,
                        endTimeForPeriod, parameterIdData);
                if (valuesPerSlice.isEmpty()) {
                    notChargedParameters.add(parameterIdData);
                } else {
                    addNotChargedParameterValues(parameterIdData,
                            valuesPerSlice, notChargedParameterValues);
                    updateParameterPeriodFactorsPerTimeSlice(valuesPerSlice);
                }
            }

            removeNotChargedParametersAndValues(parameterData,
                    notChargedParameters, notChargedParameterValues);
        }
    }

    /**
     * Add all parameter values, which don't belong to any time slice, to the
     * given map
     * 
     * @param parameterIdData
     *            the parameterIdData
     * @param valuesPerSlice
     *            a map with the parameter values per time slice
     * @param notChargedParameterValues
     *            a map with the parameterValues, which are not charged (the key
     *            is the parameterIdData object)
     */
    private void addNotChargedParameterValues(
            XParameterIdData parameterIdData,
            Map<TimeSlice, LinkedList<XParameterPeriodValue>> valuesPerSlice,
            Map<XParameterIdData, Set<XParameterPeriodValue>> notChargedParameterValues) {
        Set<XParameterPeriodValue> parValuesInSlices = new HashSet<XParameterPeriodValue>();
        for (List<XParameterPeriodValue> parValues : valuesPerSlice.values()) {
            parValuesInSlices.addAll(parValues);
        }

        Set<XParameterPeriodValue> notChargedParValueSet = new HashSet<XParameterPeriodValue>(
                parameterIdData.getPeriodValues());
        notChargedParValueSet.removeAll(parValuesInSlices);
        if (notChargedParValueSet.size() > 0) {
            notChargedParameterValues.put(parameterIdData,
                    notChargedParValueSet);
        }
    }

    /**
     * Remove all parameters and parameter values, that are not charged, from
     * the given parameter data
     * 
     * @param parameterData
     *            the parameter data
     * @param notChargedParameters
     *            a list with the parameters, that are not charged
     * @param notChargedParameterValues
     *            a map with the parameterValues, which are not charged (the key
     *            is the parameterIdData object)
     */
    private void removeNotChargedParametersAndValues(
            XParameterData parameterData,
            List<XParameterIdData> notChargedParameters,
            Map<XParameterIdData, Set<XParameterPeriodValue>> notChargedParameterValues) {

        parameterData.getIdData().removeAll(notChargedParameters);

        for (XParameterIdData parameterIdData : parameterData.getIdData()) {
            Set<XParameterPeriodValue> notChargedParValueSet = notChargedParameterValues
                    .get(parameterIdData);
            if (notChargedParValueSet != null) {
                parameterIdData.getPeriodValues().removeAll(
                        notChargedParValueSet);
            }
        }
    }

    private boolean subscriptionHasParameters(XParameterData parameterData) {
        return parameterData != null;
    }

    private Map<TimeSlice, LinkedList<XParameterPeriodValue>> mapParameterValuesToTimeSlices(
            BillingInput billingInput, XParameterData parameterData,
            long startTimeForPeriod, long endTimeForPeriod,
            XParameterIdData parameterIdData) {
        Map<TimeSlice, LinkedList<XParameterPeriodValue>> valuesPerSlice = assignParameterValuesToTimeSlices(
                billingInput, startTimeForPeriod, endTimeForPeriod,
                parameterIdData, parameterData.getPeriod());
        markLastTimeSliceHavingParameters(valuesPerSlice);
        markFirstTimeSliceHavingParameters(valuesPerSlice);
        return valuesPerSlice;
    }

    private void updateParameterPeriodFactorsPerTimeSlice(
            Map<TimeSlice, LinkedList<XParameterPeriodValue>> perSlice) {
        for (TimeSlice timeSlice : perSlice.keySet()) {
            updateParameterPeriodFactor(timeSlice, perSlice.get(timeSlice));
        }
    }

    private void resetParameterPeriodFactorsToZero(
            XParameterIdData parameterIdData) {
        for (XParameterPeriodValue periodValue : parameterIdData
                .getPeriodValues()) {
            periodValue.setPeriodFactor(0D);
        }
    }

    private Map<TimeSlice, LinkedList<XParameterPeriodValue>> assignParameterValuesToTimeSlices(
            BillingInput billingInput, long startTimeForPeriod,
            long endTimeForPeriod, XParameterIdData parameterIdData,
            PricingPeriod pricingPeriod) {

        Map<TimeSlice, LinkedList<XParameterPeriodValue>> result = new LinkedHashMap<TimeSlice, LinkedList<XParameterPeriodValue>>();
        TimeSlice timeSlice = lastPeriodTimeSlice(endTimeForPeriod,
                billingInput.getBillingPeriodEnd(), pricingPeriod);
        while (timeSliceInRange(timeSlice, startTimeForPeriod, endTimeForPeriod)) {
            LinkedList<XParameterPeriodValue> values = retrieveParametersForTimeSlice(
                    parameterIdData.getPeriodValues(), timeSlice);
            result.put(timeSlice, values);
            timeSlice = timeSlice.previous();
        }
        return result;
    }

    private void markFirstTimeSliceHavingParameters(
            Map<TimeSlice, LinkedList<XParameterPeriodValue>> result) {
        Object[] timeSlices = result.keySet().toArray();
        for (int i = timeSlices.length - 1; i > -1; i--) {
            if (result.get(timeSlices[i]).size() > 0) {
                ((TimeSlice) timeSlices[i]).setFirstSlice(true);
                break;
            }
        }
    }

    private void markLastTimeSliceHavingParameters(
            Map<TimeSlice, LinkedList<XParameterPeriodValue>> result) {
        Object[] timeSlices = result.keySet().toArray();
        for (int i = 0; i < timeSlices.length; i++) {
            if (result.get(timeSlices[i]).size() > 0) {
                ((TimeSlice) timeSlices[i]).setLastSlice(true);
                break;
            }
        }
    }

    /**
     * @param periodValues
     *            expected to be sorted descending
     */
    LinkedList<XParameterPeriodValue> retrieveParametersForTimeSlice(
            List<XParameterPeriodValue> periodValues, TimeSlice timeSlice) {

        LinkedList<XParameterPeriodValue> result = new LinkedList<XParameterPeriodValue>();
        for (XParameterPeriodValue paramValue : periodValues) {
            if (paramValue.getStartTime() >= timeSlice.getStart()
                    && paramValue.getStartTime() <= timeSlice.getEnd()) {
                result.add(paramValue);
            }
        }

        XParameterPeriodValue lastBeforePeriod = lastParamBeforeTimeSlice(
                periodValues, timeSlice.getStart());
        if (lastBeforePeriod != null) {
            result.add(lastBeforePeriod);
        }

        return result;
    }

    XParameterPeriodValue lastParamBeforeTimeSlice(
            List<XParameterPeriodValue> periodValues, long timeSliceStart) {
        for (int i = 0; i < periodValues.size(); i++) {
            XParameterPeriodValue periodValue = periodValues.get(i);
            if (periodValue.getStartTime() < timeSliceStart) {
                return periodValue;
            }
        }
        return null;
    }

    /**
     * NOTE: in order to calculate the correct factor the valuesPerSlice list
     * needs to be modified by this method. Keep this in mind.
     */
    private void updateParameterPeriodFactor(TimeSlice timeSlice,
            LinkedList<XParameterPeriodValue> valuesPerSlice) {

        if (timeSlice.isLastButNotFirst()) {
            updatePeriodFactorLastSliceLastValue(timeSlice, valuesPerSlice);
        }

        if (timeSlice.isFirstButNotLast()) {
            updatePeriodFactorFirstSliceFirstValue(timeSlice, valuesPerSlice);
        }

        if (timeSlice.isFirstAndLast()) {
            updatePeriodFactorOneSliceLastValues(timeSlice, valuesPerSlice);
        }

        for (XParameterPeriodValue parameterPeriodValue : valuesPerSlice) {
            Calendar endTimeForCalculation = parameterEndTimeForPeriodCalculation(
                    timeSlice, parameterPeriodValue);
            Calendar startTimeForCalculation = parameterStartTimeForPeriodCalculation(
                    timeSlice, parameterPeriodValue);
            updateParameterPeriodFactor(timeSlice, parameterPeriodValue,
                    startTimeForCalculation, endTimeForCalculation);
        }

    }

    /**
     * The end time of the last parameter and the start time of the first
     * parameter within the single time slice have to be extended to the time
     * slice start respectively end time.
     * 
     * @param timeSlice
     *            the single time slice having at least on parameter
     * @param valuesPerSlice
     *            all parameter values for the time slice, the list will be
     *            modified by this method
     */
    private void updatePeriodFactorOneSliceLastValues(TimeSlice timeSlice,
            LinkedList<XParameterPeriodValue> valuesPerSlice) {
        if (valuesPerSlice.size() == 1) {
            valuesPerSlice.getFirst().setPeriodFactor(1D);
            valuesPerSlice.removeFirst();
        } else {
            updatePeriodFactorLastSliceLastValue(timeSlice, valuesPerSlice);
            updatePeriodFactorFirstSliceFirstValue(timeSlice, valuesPerSlice);
        }
    }

    /**
     * The end time of the last parameter value within the last time slice has
     * to be extended to the time slice end.
     * 
     * @param timeSlice
     *            the last time slice having parameters of the billing relevant
     *            period
     * @param valuesPerSlice
     *            all parameter values for the given slice, the list will be
     *            modified by this method
     */
    private void updatePeriodFactorLastSliceLastValue(TimeSlice timeSlice,
            LinkedList<XParameterPeriodValue> valuesPerSlice) {

        // The beginning of the next time slice is relevant for the period
        // calculation, not the last millisecond of the current time slice.
        Calendar startOfNextSlice = timeSlice.getStartOfNextSliceAsCalendar();

        XParameterPeriodValue lastPeriodValue = valuesPerSlice.getFirst();
        Calendar startTime = parameterStartTimeForPeriodCalculation(timeSlice,
                lastPeriodValue);
        updateParameterPeriodFactor(timeSlice, lastPeriodValue, startTime,
                startOfNextSlice);
        valuesPerSlice.removeFirst();
    }

    /**
     * The start time of the first parameter within the first time slice has to
     * be extended to the beginning of the time slice.
     * 
     * @param timeSlice
     *            the first time slice having parameters of the billing relevant
     *            period
     * @param valuesPerSlice
     *            all parameter values for the given slice, the list will be
     *            modified by this method
     */
    private void updatePeriodFactorFirstSliceFirstValue(TimeSlice timeSlice,
            LinkedList<XParameterPeriodValue> valuesPerSlice) {
        XParameterPeriodValue firstPeriodValue = valuesPerSlice.getLast();
        updateParameterPeriodFactor(
                timeSlice,
                firstPeriodValue,
                timeSlice.getStartAsCalendar(),
                parameterEndTimeForPeriodCalculation(timeSlice,
                        firstPeriodValue));
        valuesPerSlice.removeLast();
    }

    private void updateParameterPeriodFactor(TimeSlice timeSlice,
            XParameterPeriodValue parameterPeriodValue, Calendar start,
            Calendar end) {
        double periodFactor = computeFractionalFactorForTimeUnit(
                start.getTimeInMillis(), end.getTimeInMillis(),
                timeSlice.getPeriod());
        double currentFactor = parameterPeriodValue.getPeriodFactor();
        parameterPeriodValue.setPeriodFactor(currentFactor + periodFactor);
    }

    private Calendar parameterEndTimeForPeriodCalculation(TimeSlice timeSlice,
            XParameterPeriodValue parameterPeriodValue) {

        long endTimeForCalculation = 0;
        if (parameterPeriodValue.getEndTime() <= timeSlice.getEnd()) {
            endTimeForCalculation = parameterPeriodValue.getEndTime();
        } else {
            // The beginning of the next time slice is relevant for the
            // period calculation, not the last millisecond of the
            // current time slice.
            endTimeForCalculation = timeSlice.getStartOfNextSlice();
        }

        Calendar result = Calendar.getInstance();
        result.setTimeInMillis(endTimeForCalculation);
        return result;
    }

    private Calendar parameterStartTimeForPeriodCalculation(
            TimeSlice timeSlice, XParameterPeriodValue parameterPeriodValue) {

        long startTimeForCalculation = 0;
        if (parameterPeriodValue.getStartTime() < timeSlice.getStart()) {
            startTimeForCalculation = timeSlice.getStart();
        } else {
            startTimeForCalculation = parameterPeriodValue.getStartTime();
        }

        Calendar result = Calendar.getInstance();
        result.setTimeInMillis(startTimeForCalculation);
        return result;
    }

    private TimeSlice lastPeriodTimeSlice(long endTimeForPeriod,
            long billingPeriodEnd, PricingPeriod pricingPeriod) {

        long adjustedBillingPeriodEnd = PricingPeriodDateConverter
                .getStartTime(billingPeriodEnd, pricingPeriod)
                .getTimeInMillis();
        if (endTimeForPeriod >= adjustedBillingPeriodEnd) {
            endTimeForPeriod = adjustedBillingPeriodEnd - 1;
        }
        long start = startOfTimeSlice(endTimeForPeriod, pricingPeriod);
        long end = endOfTimeSlice(endTimeForPeriod, pricingPeriod);
        return new TimeSlice(start, end, pricingPeriod);
    }

    private long startOfTimeSlice(long baseTime, PricingPeriod period) {
        return PricingPeriodDateConverter.getStartTime(baseTime, period)
                .getTimeInMillis();
    }

    private long endOfTimeSlice(long baseTime, PricingPeriod period) {
        return PricingPeriodDateConverter.getStartTimeOfNextPeriod(baseTime,
                period).getTimeInMillis() - 1;
    }

    private boolean timeSliceInRange(TimeSlice timeSlice,
            long startTimeForPeriod, long endTimeForPeriod) {
        if ((timeSlice.getStart() < startTimeForPeriod && timeSlice.getEnd() < startTimeForPeriod)
                || (timeSlice.getStart() > endTimeForPeriod && timeSlice
                        .getEnd() > endTimeForPeriod)) {
            return false;
        }
        return true;
    }

    @Override
    public boolean isSuspendedAndResumedInSameTimeUnit(
            SubscriptionHistory current, SubscriptionHistory next,
            PriceModelHistory pm) {
        // is suspended and resumed in the same time unit? If so, ignore this
        // history entry, so that it is not charged twice!
        if (current.getStatus().isSuspendedOrSuspendedUpd()
                && next.getStatus().isActiveOrPendingUpd()
                && current.getProductObjKey() == next.getProductObjKey()
                && PricingPeriodDateConverter.getStartTime(
                        current.getModdate().getTime(), pm.getPeriod())
                        .getTimeInMillis() == PricingPeriodDateConverter
                        .getStartTime(next.getModdate().getTime(),
                                pm.getPeriod()).getTimeInMillis()) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public BigDecimal calculateParameterUserCosts(
            XParameterPeriodValue parameterPeriodValue,
            BigDecimal valueMultplier) {
        BigDecimal costs = parameterPeriodValue.getPricePerUser().multiply(
                valueMultplier);
        costs = BigDecimals.multiply(costs,
                parameterPeriodValue.getUserAssignmentFactor());
        return costs;
    }

    @Override
    public void computeParameterUserFactorAndRoleFactor(
            BillingDataRetrievalServiceLocal billingDao, BillingInput input,
            XParameterData parameterData, long startTimeForPeriod,
            long endTimeForPeriod) {

        resetUserFactors(parameterData);
        final List<UsageLicenseHistory> ulHistList = billingDao
                .loadUsageLicenses(input.getSubscriptionKey(),
                        startTimeForPeriod, endTimeForPeriod);
        List<TimeSlice> relevantTimeSlices = relevantTimeSlices(input,
                startTimeForPeriod, endTimeForPeriod, parameterData.getPeriod());
        Collection<XParameterIdData> parameterIds = parameterData.getIdData();
        for (XParameterIdData parameterIdData : parameterIds) {
            computeParameterUserFactorForParameterId(parameterIdData,
                    relevantTimeSlices, ulHistList);
        }
    }

    /**
     * Sets user assignment factors and role factors to zero.
     */
    private void resetUserFactors(XParameterData parameterData) {
        for (XParameterIdData parameterIdData : parameterData.getIdData()) {
            for (XParameterPeriodValue periodValue : parameterIdData
                    .getPeriodValues()) {
                periodValue.setUserAssignmentFactor(0);
                if (periodValue.getRolePrices() != null) {
                    Set<Long> containerKeys = periodValue.getRolePrices()
                            .getContainerKeys();
                    for (Long containerKey : containerKeys) {
                        Map<Long, RolePricingDetails> rolePricesForContainerKey = periodValue
                                .getRolePrices().getRolePricesForContainerKey(
                                        containerKey);
                        for (Long k : rolePricesForContainerKey.keySet()) {
                            rolePricesForContainerKey.get(k).setFactor(0D);
                        }
                    }
                }
            }
        }
    }

    /**
     * Creates a list of time slices for the given period. Each time slice holds
     * the period type (week, day etc.) and its start and end date. <br />
     */
    List<TimeSlice> relevantTimeSlices(BillingInput input,
            long startTimeForPeriod, long endTimeForPeriod, PricingPeriod period) {

        List<TimeSlice> timeSlices = new ArrayList<TimeSlice>();
        TimeSlice timeSlice = lastPeriodTimeSlice(endTimeForPeriod,
                input.getBillingPeriodEnd(), period);
        long timeForBeginningOfTimeSlices = PricingPeriodDateConverter
                .getStartTime(input.getBillingPeriodStart(), period)
                .getTimeInMillis();
        if (timeForBeginningOfTimeSlices < startTimeForPeriod) {
            timeForBeginningOfTimeSlices = startTimeForPeriod;
        }
        while (timeSlice.getEnd() > timeForBeginningOfTimeSlices) {
            timeSlices.add(timeSlice);
            timeSlice = timeSlice.previous();
        }
        return timeSlices;
    }

    private void computeParameterUserFactorForParameterId(
            XParameterIdData parameterIdData, List<TimeSlice> timeSlices,
            List<UsageLicenseHistory> ulHistList) {

        for (TimeSlice timeSlice : timeSlices) {
            LinkedList<XParameterPeriodValue> periodValueForTimeSlice = retrieveParametersForTimeSlice(
                    parameterIdData.getPeriodValues(), timeSlice);
            UserAssignmentExtractor uaExtractor = new UserAssignmentExtractor(
                    ulHistList, timeSlice.getStart(), timeSlice.getEnd());
            uaExtractor.extract();
            for (Long userKey : uaExtractor.getUserKeys()) {
                computeParameterUserFactorForOneTimeSliceAndOneUser(timeSlice,
                        periodValueForTimeSlice,
                        uaExtractor.getUserAssignments(userKey));
            }
        }
    }

    void computeParameterUserFactorForOneTimeSliceAndOneUser(
            TimeSlice timeSlice,
            LinkedList<XParameterPeriodValue> periodValues,
            List<UserAssignment> userAssignmentsForOneUser) {

        List<XParameterPeriodValue> periodValuesHavingUserAssignment = determinePeriodValuesHavingUserAssignment(
                periodValues, userAssignmentsForOneUser, timeSlice.getPeriod());

        long endTime = timeSlice.getEnd();
        int index = 0;
        for (XParameterPeriodValue periodValue : periodValuesHavingUserAssignment) {
            boolean isLastPeriodValue = (index == 0);
            boolean isFirstPeriodValue = (index == periodValuesHavingUserAssignment
                    .size() - 1);

            long startTime;
            if (isFirstPeriodValue) {
                startTime = timeSlice.getStart();
            } else {
                startTime = determineUserAssignmentStartTimeForPeriodValue(
                        userAssignmentsForOneUser, periodValue);
            }

            UserAssignmentFactors factors = computeParameterUserFactorForOnePeriodValue(
                    timeSlice, userAssignmentsForOneUser, startTime, endTime,
                    isFirstPeriodValue, isLastPeriodValue);

            periodValue.setUserAssignmentFactor(periodValue
                    .getUserAssignmentFactor() + factors.getBasicFactor());
            updateRoleFactorForPeriodValue(periodValue, factors);

            endTime = startTime;
            index++;
        }
    }

    private List<XParameterPeriodValue> determinePeriodValuesHavingUserAssignment(
            LinkedList<XParameterPeriodValue> periodValues,
            List<UserAssignment> userAssignments, PricingPeriod period) {
        List<XParameterPeriodValue> periodValuesHavingUserAssignment = new ArrayList<>();
        int index = 0;

        for (XParameterPeriodValue periodValue : periodValues) {
            long periodEnd = periodValue.getEndTime();
            // Extend the last parameter value to the end of the time slice
            if (index == 0) {
                periodEnd = PricingPeriodDateConverter
                        .getStartTimeOfNextPeriod(periodEnd, period)
                        .getTimeInMillis();
            }

            if (userAssignmentExistInPeriod(userAssignments,
                    periodValue.getStartTime(), periodEnd)) {
                periodValuesHavingUserAssignment.add(periodValue);
            }
            index++;
        }

        return periodValuesHavingUserAssignment;
    }

    boolean userAssignmentExistInPeriod(List<UserAssignment> userAssignments,
            long periodStart, long periodEnd) {
        for (UserAssignment userAssignment : userAssignments) {
            if (isUserAssignmentInPeriod(userAssignment, periodStart, periodEnd)) {
                return true;
            }
        }
        return false;
    }

    private long determineUserAssignmentStartTimeForPeriodValue(
            List<UserAssignment> userAssignmentsForOneUser,
            XParameterPeriodValue periodValue) {
        UserAssignment oldestUserAssignment = findOldestUserAssignmentForPeriod(
                userAssignmentsForOneUser, periodValue.getStartTime(),
                periodValue.getEndTime());
        long startTime = periodValue.getStartTime();
        if (startTime < oldestUserAssignment.getUsageStartTime()) {
            startTime = oldestUserAssignment.getUsageStartTime();
        }
        return startTime;
    }

    UserAssignment findOldestUserAssignmentForPeriod(
            List<UserAssignment> userAssignments, long periodStart,
            long periodEnd) {
        UserAssignment oldestUserAssignment = null;
        for (UserAssignment userAssignment : userAssignments) {
            if (isUserAssignmentInPeriod(userAssignment, periodStart, periodEnd)) {
                oldestUserAssignment = userAssignment;
            }
        }
        return oldestUserAssignment;
    }

    boolean isUserAssignmentInPeriod(UserAssignment userAssignment,
            long periodStart, long periodEnd) {
        if (userAssignment.getUsageStartTime() < periodEnd
                && userAssignment.getUsageEndTime() >= periodStart) {
            return true;
        }
        return false;
    }

    private UserAssignmentFactors computeParameterUserFactorForOnePeriodValue(
            TimeSlice timeSlice, List<UserAssignment> userAssignments,
            long periodStart, long periodEnd, boolean needsExtendToSliceStart,
            boolean needsExtendToSliceEnd) {

        String userId = userAssignments.get(0).getUserId();
        long userKey = userAssignments.get(0).getUserKey();

        double factor = computeFactorForTimeSlice(timeSlice, periodStart,
                periodEnd, needsExtendToSliceStart, needsExtendToSliceEnd);
        UserAssignmentFactors result = new UserAssignmentFactors();
        storeUserFactorToResult(result, factor, userKey, userId);

        computeParameterUserRoleFactorForOnePeriodValue(result, userKey,
                timeSlice, userAssignments, periodStart, periodEnd,
                needsExtendToSliceStart, needsExtendToSliceEnd);
        return result;
    }

    private UserAssignmentFactors computeParameterUserRoleFactorForOnePeriodValue(
            UserAssignmentFactors result, long userKey, TimeSlice timeSlice,
            List<UserAssignment> userAssignments, long periodStart,
            long periodEnd, boolean needsExtendToSliceStart,
            boolean needsExtendToSliceEnd) {

        List<UserRoleAssignment> roleAssignments = determineRoleAssignmentsForOnePeriodValue(
                userAssignments, periodStart, periodEnd);

        long endTime = periodEnd;
        int index = 0;
        for (UserRoleAssignment roleAssignment : roleAssignments) {
            boolean isLastRoleForPeriod = (index == 0);
            boolean isFirstRoleForPeriod = (index == roleAssignments.size() - 1);

            long startTime = roleAssignment.getStartTime();
            if (startTime < periodStart) {
                startTime = periodStart;
            }

            double factor = computeFactorForTimeSlice(timeSlice, startTime,
                    endTime, needsExtendToSliceStart && isFirstRoleForPeriod,
                    needsExtendToSliceEnd && isLastRoleForPeriod);
            storeUserRoleFactorToResult(result, roleAssignment, factor, userKey);

            endTime = startTime;
            index++;
        }
        return result;
    }

    private List<UserRoleAssignment> determineRoleAssignmentsForOnePeriodValue(
            List<UserAssignment> userAssignments, long periodStart,
            long periodEnd) {

        List<UserRoleAssignment> result = new ArrayList<UserRoleAssignment>();
        List<UserRoleAssignment> roleAssignments = determineRoleAssignmentsWithFillingBlank(userAssignments);
        for (UserRoleAssignment roleAssignment : roleAssignments) {
            if (isUserRoleAssignmentInPeriod(roleAssignment, periodStart,
                    periodEnd)) {
                result.add(roleAssignment);
            }
        }
        return result;
    }

    boolean isUserRoleAssignmentInPeriod(UserRoleAssignment roleAssignment,
            long periodStart, long periodEnd) {
        if (roleAssignment.getStartTime() < periodEnd
                && roleAssignment.getEndTime() >= periodStart) {
            return true;
        }
        return false;
    }

    void updateRoleFactorForPeriodValue(XParameterPeriodValue periodValue,
            UserAssignmentFactors factors) {

        Map<Long, Double> roleFactors = factors.getRoleFactors();

        if (roleFactors.isEmpty() || parameterRolePriceNotDefined(periodValue)) {
            return;
        }

        RolePricingData pricingData = periodValue.getRolePrices();
        Long priceParamKey = periodValue.getKey();
        Map<Long, RolePricingDetails> rolePrices = pricingData
                .getRolePricesForContainerKey(priceParamKey);
        for (Long roleKey : roleFactors.keySet()) {
            if (parameterRolePriceDefined(rolePrices, roleKey)) {
                RolePricingDetails rolePricingDetails = rolePrices.get(roleKey);
                double currentFactor = rolePricingDetails.getFactor();
                rolePrices.get(roleKey).setFactor(
                        currentFactor + roleFactors.get(roleKey).doubleValue());
            }
        }
    }

    boolean parameterRolePriceNotDefined(XParameterPeriodValue periodValue) {
        return periodValue.getRolePrices() == null;
    }

    boolean parameterRolePriceDefined(Map<Long, RolePricingDetails> rolePrices,
            Long roleKey) {
        return rolePrices.get(roleKey) != null;
    }

}