## $Id: utils.py 13306 2015-10-12 05:06:37Z henrik $
##
## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
import grok
import random
from time import time
from zope.component import createObject, getUtility
from zope.catalog.interfaces import ICatalog
from waeup.kofa.interfaces import CLEARED, RETURNING, PAID
from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
from waeup.kofa.accesscodes import create_accesscode
from waeup.kofa.interfaces import (
    CLEARED, RETURNING, ADMITTED, PAID, IKofaUtils)
from waeup.kofa.fees import FeeTable
from waeup.kofa.hostels.hostel import NOT_OCCUPIED
from waeup.kwarapoly.interfaces import MessageFactory as _


# 10  = PreND (1)
# 100 = ND1 (2)
# 110 = ND1R (3)
# 200 = ND2 (4)
# 210 = ND2R (5)
# 300 = ND3 (6)
# 400 = HND1 (7)
# 410 = HND1R (8)
# 500 = HND2 (9)
# 510 = HND2R (10)
# 600 = HND3 (11)
# 999 = PGD (12)

PAYMENT_LEVELS = (10, 100, 110, 200, 210, 300, 400, 410, 500, 510, 600, 999)

FEES_PARAMS = (
        ('ft', 'we'),
        ('local', 'non-local'),
        ('science','arts'),
        PAYMENT_LEVELS
    )

FEES_VALUES = (
        (
          ( # 10       100      110      200      210     300    400     410      500      510     600   999
            (34500.0, 40700.0, 33600.0, 31500.0, 33600.0, 0.0, 46200.0, 35600.0, 34700.0, 35600.0, 0.0, 48750.0), # science
            (34500.0, 38700.0, 31600.0, 29500.0, 31600.0, 0.0, 44200.0, 33600.0, 32700.0, 33600.0, 0.0, 47200.0)  # arts
          ), # local
          ( # 10       100      110      200      210     300    400     410      500      510     600   999
            (49600.0, 55200.0, 41100.0, 34090.0, 41100.0, 0.0, 60700.0, 45600.0, 37900.0, 45600.0, 0.0, 63180.0), # science
            (49600.0, 53200.0, 39100.0, 32090.0, 39100.0, 0.0, 58700.0, 43600.0, 35900.0, 43600.0, 0.0, 61680.0)  # arts
          ), # non-local
        ), # ft
        (
          ( # 10    100       110    200      210      300      400        410    500     510       600     999
            (0.0, 42000.0, 33600.0, 33400.0, 33600.0, 33400.0, 42400.0, 35600.0, 37500.0, 35600.0, 37500.0, 0.0), # science
            (0.0, 40000.0, 31600.0, 31400.0, 31600.0, 31400.0, 40400.0, 33600.0, 35500.0, 33600.0, 35500.0, 0.0)  # arts
          ), # local
          ( # 10   100         110    200       210      300      400     410      500     510      600     999
            (0.0, 56700.0, 41100.0, 36350.0, 41100.0, 36350.0, 57600.0, 45600.0, 45850.0, 45600.0, 45850.0, 0.0), # science
            (0.0, 54700.0, 39100.0, 34350.0, 39100.0, 34350.0, 55600.0, 43600.0, 43850.0, 43600.0, 43850.0, 0.0)  # arts
          ), # non-local
        ), # we
    )

SCHOOL_FEES = FeeTable(FEES_PARAMS, FEES_VALUES)

def local_nonlocal(student):
    lga = getattr(student, 'lga')
    if lga and lga.startswith('kwara'):
        return 'local'
    else:
        return 'non-local'

def arts_science(student):
    if student.faccode == 'IFMS':
        return 'arts'
    else:
        return 'science'

def we_ft(student):
    if student.current_mode.endswith('we'):
        return 'we'
    else:
        return 'ft'

class CustomStudentsUtils(NigeriaStudentsUtils):
    """A collection of customized methods.

    """

    def maxCredits(self, studylevel):
        # Students do not have any credit load limit
        return None

    def selectBed(self, available_beds):
        """Randomly select a bed from a list of available beds.

        """
        return random.choice(available_beds)

    def getReturningData(self, student):
        """ This method defines what happens after school fee payment
        of returning students depending on the student's senate verdict.
        """
        prev_level = student['studycourse'].current_level
        cur_verdict = student['studycourse'].current_verdict
        if cur_verdict in ('A','B','L','M','N','Z',):
            # Successful student
            new_level = divmod(int(prev_level),100)[0]*100 + 100
        elif cur_verdict == 'C':
            # Student on probation
            new_level = int(prev_level) + 10
        else:
            # Student is somehow in an undefined state.
            # Level has to be set manually.
            new_level = prev_level
        new_session = student['studycourse'].current_session + 1
        return new_session, new_level

    def _maintPaymentMade(self, student, session):

        # Maint payment checking disabled on 5th Dec 2014
        return True

        #if len(student['payments']):
        #    for ticket in student['payments'].values():
        #        if ticket.p_category == 'hostel_maintenance' and \
        #            ticket.p_session == session and ticket.p_state == 'paid':
        #                return True
        #return False

    def _bedAvailable(self, student):
        acc_details  = self.getAccommodationDetails(student)
        cat = getUtility(ICatalog, name='beds_catalog')
        entries = cat.searchResults(
            owner=(student.student_id,student.student_id))
        if len(entries):
            # Bed has already been booked.
            return True
        entries = cat.searchResults(
            bed_type=(acc_details['bt'],acc_details['bt']))
        available_beds = [
            entry for entry in entries if entry.owner == NOT_OCCUPIED]
        if available_beds:
            # Bed has not yet been booked but beds are available.
            return True
        return False

    def _isPaymentDisabled(self, p_session, category, student):
        academic_session = self._getSessionConfiguration(p_session)
        if category == 'schoolfee':
            if 'sf_all' in academic_session.payment_disabled:
                return True
            if 'sf_non_pg' in academic_session.payment_disabled and \
                not student.is_postgrad:
                return True
        return False

    def setPaymentDetails(self, category, student,
            previous_session=None, previous_level=None):
        """Create Payment object and set the payment data of a student for
        the payment category specified.

        """
        details = {}
        p_item = u''
        amount = 0.0
        error = u''
        if previous_session:
            return _('Previous session payment not yet implemented.'), None
        p_session = student['studycourse'].current_session
        p_level = student['studycourse'].current_level
        p_current = True
        academic_session = self._getSessionConfiguration(p_session)
        if academic_session == None:
            return _(u'Session configuration object is not available.'), None
        # Determine fee.
        if category == 'transfer':
            amount = academic_session.transfer_fee
        elif category == 'gown':
            amount = academic_session.gown_fee
        elif category == 'bed_allocation':
            amount = academic_session.booking_fee
        elif category == 'hostel_maintenance':
            amount = 0.0
            bedticket = student['accommodation'].get(
                str(student.current_session), None)
            if bedticket:
                p_item = bedticket.bed_coordinates
                if bedticket.bed.__parent__.maint_fee > 0:
                    amount = bedticket.bed.__parent__.maint_fee
                else:
                    # fallback
                    amount = academic_session.maint_fee
            else:
                # Should not happen because this is already checked
                # in the browser module, but anyway ...
                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
                p_item = trans(_('no bed allocated'), portal_language)
        elif category == 'clearance':
            amount = academic_session.clearance_fee
            try:
                p_item = student['studycourse'].certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
        elif category == 'schoolfee':
            try:
                certificate = student['studycourse'].certificate
                p_item = certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
            if student.state == RETURNING:
                # Override p_session and p_level
                p_session, p_level = self.getReturningData(student)
                academic_session = self._getSessionConfiguration(p_session)
                if academic_session == None:
                    return _(u'Session configuration object '
                              'is not available.'), None
            if student.state == CLEARED and student.current_mode in (
                                                            'hnd_ft', 'nd_ft'):
                # Fresh students must have booked and paid for accommodation.
                if self._bedAvailable(student):
                    if not student.faccode == 'IOT' and \
                        not self._maintPaymentMade(student, p_session):
                        return _('Book and pay for accommodation first '
                                 'before making school fee payments.'), None
            if student.state in (RETURNING, CLEARED):
                if p_level in PAYMENT_LEVELS:
                    amount = SCHOOL_FEES.get_fee(
                        (we_ft(student),
                         local_nonlocal(student),
                         arts_science(student),
                         p_level)
                        )
        elif category == 'carryover1':
            amount = 6000.0
        elif category == 'carryover2':
            amount = 10000.0
        elif category == 'carryover3':
            amount = 15000.0

        else:
            fee_name = category + '_fee'
            amount = getattr(academic_session, fee_name, 0.0)
        if amount in (0.0, None):
            return _(u'Amount could not be determined.'), None
        if self.samePaymentMade(student, category, p_item, p_session):
            return _('This type of payment has already been made.'), None
        # Add session specific penalty fee.
        if category == 'schoolfee' and student.is_postgrad:
            amount += academic_session.penalty_pg
        elif category == 'schoolfee':
            amount += academic_session.penalty_ug
        # Recategorize carryover fees.
        if category.startswith('carryover'):
            p_item = getUtility(IKofaUtils).PAYMENT_CATEGORIES[category]
            p_item = unicode(p_item)
            # Now we change the category to reduce the number of categories.
            category = 'schoolfee'
        if self._isPaymentDisabled(p_session, category, student):
            return _('Payment temporarily disabled.'), None
        payment = createObject(u'waeup.StudentOnlinePayment')
        timestamp = ("%d" % int(time()*10000))[1:]
        payment.p_id = "p%s" % timestamp
        payment.p_category = category
        payment.p_item = p_item
        payment.p_session = p_session
        payment.p_level = p_level
        payment.p_current = p_current
        payment.amount_auth = float(amount)
        return None, payment

    def getAccommodationDetails(self, student):
        """Determine the accommodation data of a student.
        """
        d = {}
        d['error'] = u''
        hostels = grok.getSite()['hostels']
        d['booking_session'] = hostels.accommodation_session
        d['allowed_states'] = hostels.accommodation_states
        d['startdate'] = hostels.startdate
        d['enddate'] = hostels.enddate
        d['expired'] = hostels.expired
        # Determine bed type
        studycourse = student['studycourse']
        certificate = getattr(studycourse,'certificate',None)
        current_level = studycourse.current_level
        if None in (current_level, certificate):
            return d
        end_level = certificate.end_level
        if current_level == 10:
            bt = 'pr'
        elif current_level in (100, 400):
            bt = 'fr'
        elif current_level in (300, 600):
            bt = 'fi'
        else:
            bt = 're'
        if student.sex == 'f':
            sex = 'female'
        else:
            sex = 'male'
        special_handling = 'regular'
        if student.faccode == 'IOT':
            special_handling = 'iot'
        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
        return d

    def increaseMatricInteger(self, student):
        """Increase counter for matric numbers.
        """
        cert = getattr(student.get('studycourse', None), 'certificate', None)
        entry_session = getattr(
            student.get('studycourse', None), 'entry_session', None)
        dep = cert.__parent__.__parent__
        next_matric_dict = getattr(dep, 'next_matric_dict', None)
        if not next_matric_dict:
            # add next_matric_dict attribute
            dep.next_matric_dict = {}
        if not dep.next_matric_dict.get(entry_session, None):
            # initialize counter element
            dep.next_matric_dict[entry_session] = 2
            return
        # increase counter value of entry_session
        dep.next_matric_dict[entry_session] += 1
        return

    def constructMatricNumber(self, student):
        """Fetch the matric number counter which fits the student and
        construct the new matric number of the student.

        A typical matriculation number is like this: ND/14/STA/FT/015

        ND = Study Mode Prefix
        14 = Year of Entry
        STA = Department Code
        FT = Study Mode Suffix
        015 = Serial Number (Every department starts from "001" every
        session and the numbers build up arithmetically)
        """
        cert = getattr(student.get('studycourse', None), 'certificate', None)
        entry_session = getattr(
            student.get('studycourse', None), 'entry_session', None)
        entry_mode = getattr(
            student.get('studycourse', None), 'entry_mode', None)
        if entry_session < 2015:
            return _('Available from session 2015/2016'), None
        if student.state not in (PAID, ):
            return _('Wrong state.'), None
        if None in (cert, entry_session, entry_mode):
            return _('Matriculation number cannot be set.'), None
        try:
            (prefix, suffix) = entry_mode.split('_')
            (prefix, suffix) = (prefix.upper(), suffix.upper())
        except ValueError:
            return _('Matriculation number cannot be set.'), None
        dep = cert.__parent__.__parent__
        next_integer = getattr(
            dep, 'next_matric_dict', {}).get(entry_session, 1)
        entry_year = entry_session - 100*(entry_session/100)
        matric_number = "%s/%s/%s/%s/%03d" % (
            prefix, entry_year, student.depcode, suffix, next_integer)
        return None, matric_number

    PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARED, RETURNING)

    # KwaraPoly prefix
    STUDENT_ID_PREFIX = u'W'
