#
# SchoolTool - common information systems platform for school administration
# Copyright (c) 2005 Shuttleworth Foundation
#
# 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
#
"""
SchoolTool section views
"""

from collections import defaultdict

from zope.interface import implements, Invalid
from zope.i18n import translate
from zope.security.proxy import removeSecurityProxy
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from zope.app.intid.interfaces import IIntIds
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.publisher.browser import BrowserView
from zope.component import adapts
from zope.component import getUtility
from zope.component import getMultiAdapter
from zope.app.container.contained import NameChooser
from zope.app.container.interfaces import INameChooser
from zope.event import notify
from zope.schema import Choice
from zc.table import table
from zope.traversing.browser.interfaces import IAbsoluteURL
from zope.traversing.browser.absoluteurl import absoluteURL
from z3c.form import form, subform, field, datamanager, button
from z3c.form.action import ActionErrorOccurred
from z3c.form.interfaces import ActionExecutionError

from schooltool.common import SchoolToolMessage as _
from schooltool.person.interfaces import IPerson
from schooltool.term.interfaces import ITerm
from schooltool.timetable.interfaces import ITimetables
from schooltool.schoolyear.interfaces import ISchoolYear
from schooltool.skin.containers import ContainerView
from schooltool.app.browser.app import BaseEditView
from schooltool.term.interfaces import IDateManager
from schooltool.term.term import getPreviousTerm, getNextTerm
from schooltool.term.term import listTerms
from schooltool.app.utils import vocabulary_titled
from schooltool.course.interfaces import ICourse, ICourseContainer
from schooltool.course.interfaces import ISection, ISectionContainer
from schooltool.course.section import Section
from schooltool.course.section import copySection
from schooltool.app.browser.app import RelationshipViewBase
from schooltool.timetable.browser import TimetableConflictMixin
from schooltool.app.interfaces import ISchoolToolApplication
from schooltool.table.interfaces import ITableFormatter


class SectionContainerAbsoluteURLAdapter(BrowserView):

    adapts(ISectionContainer, IBrowserRequest)
    implements(IAbsoluteURL)

    def __str__(self):
        container_id = int(self.context.__name__)
        int_ids = getUtility(IIntIds)
        container = int_ids.getObject(container_id)
        url = str(getMultiAdapter((container, self.request), name='absolute_url'))
        return url + '/sections'

    __call__ = __str__


class SectionContainerView(ContainerView):
    """A Course Container view."""

    __used_for__ = ISectionContainer

    @property
    def term(self):
        return ITerm(self.context)

    @property
    def school_year(self):
        return ISchoolYear(self.context)

    # XXX: very hacky, but necessary for now. :-(
    def getTimetables(self, obj):
        tt_adapter = ITimetables(obj, None)
        if tt_adapter is not None:
            timetables = sorted(tt_adapter.timetables.items())
            return [timetable
                    for key, timetable in timetables]
        return []


class SectionCopyingView(SectionContainerView):
    """A view to copy sections from the previous term."""

    __used_for__ = ISectionContainer

    @property
    def container(self):
        term = self.prev_term
        if term is None:
            return None
        return ISectionContainer(term)

    @property
    def prev_term(self):
        return getPreviousTerm(self.term)

    @property
    def select_all_boxes_js(self):
        if self.container is None:
            return ''
        keys = ['"copy.%s"' % section.__name__
                for section in self.batch
                if not section.next]
        return """
            var section_keys = new Array(
                %s
            );

            function selectAll(value) {
                for (key in section_keys) {
                    box = document.getElementById(section_keys[key]);
                    if (box) {
                        box.checked = value;
                    }
                }
            }
        """ % ','.join(keys)

    def update(self):
        if self.container is None:
            return
        if 'COPY' in self.request:
            for name, section in self.container.items():
                key = 'copy.%s' % name
                if key in self.request and not section.next:
                    section = removeSecurityProxy(section)
                    new_section = copySection(section, self.term)
                    section.next = new_section
        SectionContainerView.update(self)


class SectionLinkNextView(SectionContainerView):
    """A view to link a section to one in the next term."""

    __used_for__ = ISectionContainer

    error = u''

    @property
    def container(self):
        term = self.target_term
        if term is None:
            return None
        return ISectionContainer(term)

    @property
    def target_term(self):
        return getNextTerm(self.term)

    def link(self, target_section):
        section = removeSecurityProxy(self.context)
        target_section = removeSecurityProxy(target_section)
        section.next = target_section

    def update(self):
        if 'CANCEL' in self.request:
            self.request.response.redirect(self.nextURL())
        if self.container is None:
            return
        if 'LINK' in self.request:
            section_name = self.request.get('LINK_SECTION')
            section = self.container.get(section_name)
            if section_name is None or section is None:
                self.error = translate(_('No section selected.'),
                                       context=self.request)
            else:
                self.link(section)
                self.request.response.redirect(self.nextURL())
        SectionContainerView.update(self)

    def nextURL(self):
        return '%s/@@edit.html' % absoluteURL(self.context, self.request)


class SectionLinkPreviousView(SectionLinkNextView):
    """A view to link a section to one in the previous term."""

    @property
    def target_term(self):
        return getPreviousTerm(self.term)

    def link(self, target_section):
        section = removeSecurityProxy(self.context)
        target_section = removeSecurityProxy(target_section)
        section.previous = target_section



class SectionView(BrowserView):
    """A view for courses providing a list of sections."""

    __used_for__ = ISection

    @property
    def term(self):
        return ITerm(self.context)

    @property
    def school_year(self):
        return ISchoolYear(self.context)

    def renderPersonTable(self):
        persons = ISchoolToolApplication(None)['persons']
        formatter = getMultiAdapter((persons, self.request), ITableFormatter)
        formatter.setUp(table_formatter=table.StandaloneFullFormatter,
                        items=[removeSecurityProxy(person)
                               for person in self.context.members],
                        batch_size=0)
        return formatter.render()


class SectionNameChooser(NameChooser):

    implements(INameChooser)

    def chooseName(self, name, obj):
        """See INameChooser."""

        i = 1
        n = "1"
        while n in self.context:
            i += 1
            n = unicode(i)
        # Make sure the name is valid
        self.checkName(n, obj)
        return n


class SectionAddView(form.AddForm):
    """A view for adding Sections."""

    default_term = None
    default_course = None
    subforms = None
    _created_obj = None

    template = ViewPageTemplateFile('templates/section_add.pt')

    # Note that we also omit the title field, it will be auto-generated
    # See concerns raised in https://bugs.launchpad.net/schooltool/+bug/389283
    fields = field.Fields(ISection).omit(
        'title',
        'label', 'instructors', 'members', 'courses', 'size',
        'previous', 'next', 'linked_sections')

    def update(self):
        super(SectionAddView, self).update()
        self._finishedAdd = False

        self.course_subform = NewSectionCoursesSubform(
            self.context, self.request, self,
            default_course=self.default_course)
        self.term_subform = NewSectionTermsSubform(
            self.context, self.request, self,
            default_term=self.default_term)

        self.course_subform.update()
        self.term_subform.update()

        if (self.course_subform.errors or
            self.term_subform.errors or
            self.widgets.errors):
            self.status = self.formErrorsMessage
        elif self._created_obj:
            self.add(self._created_obj)
            self._finishedAdd = True

    def create(self, data):
        section = Section()
        form.applyChanges(self, section, data)
        return section

    def add(self, section):
        """Add `contact` to the container.

        Uses the username of `contact` as the object ID (__name__).
        """

        course = self.course_subform.course
        terms = self.term_subform.terms
        if course is None or not terms:
            return None

        # add the section to the first term
        sections = ISectionContainer(terms[0])
        name = INameChooser(sections).chooseName('', section)
        sections[name] = section
        section.courses.add(removeSecurityProxy(course))

        # overwrite section title.
        section.title = "%s (%s)" % (course.title, section.__name__)

        # copy and link section in other selected terms
        for term in terms[1:]:
            new_section = copySection(section, term)
            new_section.previous = section
            section = new_section
        self._finishedAdd = False

    def nextURL(self):
        return absoluteURL(self.context, self.request)

    @button.buttonAndHandler(_('Add'), name='add')
    def handleAdd(self, action):
        data, errors = self.extractData()
        if errors:
            self.status = self.formErrorsMessage
            return
        # Note we skip immediate adding of the object,
        # because we want to check subforms for errors first.
        self._created_obj = self.create(data)

    @button.buttonAndHandler(_("Cancel"), name='cancel')
    def handle_cancel_action(self, action):
        url = absoluteURL(self.context, self.request)
        self.request.response.redirect(self.nextURL())

    def updateActions(self):
        super(SectionAddView, self).updateActions()
        self.actions['add'].addClass('button-ok')
        self.actions['cancel'].addClass('button-cancel')


class NewSectionTermsSubform(subform.EditSubForm):
    template = ViewPageTemplateFile('templates/basic_subform.pt')
    prefix = 'terms'

    errors = None
    span = None

    def __init__(self, *args, **kw):
        default_term = kw.pop('default_term', None)
        super(NewSectionTermsSubform, self).__init__(*args, **kw)

        if default_term is None:
            selected_year = removeSecurityProxy(ISchoolYear(self.context))
            active_term = getUtility(IDateManager).current_term
            if (active_term is not None and
                ISchoolYear(active_term) is selected_year):
                default_term = active_term

        self.setUpFields(default_term)

    def setUpFields(self, default_term):
        terms = listTerms(self.context)
        self.vocabulary=vocabulary_titled(terms)
        self.span = defaultdict(lambda:default_term)
        self._addTermChoice('starts', _("Starts in term"))
        self._addTermChoice('ends', _("Ends in term"))

    def _addTermChoice(self, name, title):
        schema_field = Choice(
            __name__=name, title=title,
            required=True,
            vocabulary=self.vocabulary,
            default=self.span[name])

        self.fields += field.Fields(schema_field)

    def getContent(self):
        return self.span

    @property
    def terms(self):
        if self.errors:
            return []
        terms = listTerms(self.context)
        return [term for term in terms
                if (term.first >= self.span['starts'].first and
                    term.last <= self.span['ends'].last)]

    @button.handler(SectionAddView.buttons['add'])
    def handleAdd(self, action):
        data, self.errors = self.widgets.extract()
        if self.errors:
            return
        changed = form.applyChanges(self, self.getContent(), data)
        starts = self.span['starts']
        ends = self.span['ends']
        if starts.first > ends.first:
            # XXX: this is a workaround for a bug in z3c.form: subforms do
            #      not handle action execution errors properly.
            #      The bug is fixed in z3c.form 2.0, as far as I know.
            widget_name = '%s.widgets.ends:list' % self.prefix
            error = ActionExecutionError(Invalid(
                _('Starting term ($starts_in) is later than ending term ($ends_in)',
                  mapping={'starts_in': starts.title,
                           'ends_in': ends.title})))
            notify(ActionErrorOccurred(action, error))


# XXX: TODO: Add "--no value--" to the course selector dropdown in
#      NewSectionCoursesSubform, once we depend on z3c.form 2.0.


class NewSectionCoursesSubform(subform.EditSubForm):
    template = ViewPageTemplateFile('templates/basic_subform.pt')
    prefix = 'courses'

    errors = None
    values = None

    def __init__(self, *args, **kw):
        default_course = kw.pop('default_course', None)
        super(NewSectionCoursesSubform, self).__init__(*args, **kw)
        courses = ICourseContainer(self.context)
        self.vocabulary=vocabulary_titled(courses.values())
        self.values = {'course': default_course}
        schema_field = Choice(
            __name__='course', title=_('Course'),
            required=True, vocabulary=self.vocabulary)
        self.fields += field.Fields(schema_field)
        datamanager.DictionaryField(self.values, schema_field)

    def getContent(self):
        return self.values

    @property
    def course(self):
        return self.values.get('course')

    @button.handler(SectionAddView.buttons['add'])
    def handleAdd(self, action):
        data, self.errors = self.widgets.extract()
        if self.errors:
            return
        changed = form.applyChanges(self, self.getContent(), data)


class AddSectionForTerm(SectionAddView):
    """A view for adding Sections for a term."""

    form.extends(SectionAddView)

    def __init__(self, *args, **kw):
        super(AddSectionForTerm, self).__init__(*args, **kw)
        self.default_term = ITerm(self.context)


class AddSectionForCourse(SectionAddView):
    """A view for adding Sections for a course."""

    form.extends(SectionAddView)

    def __init__(self, *args, **kw):
        super(AddSectionForCourse, self).__init__(*args, **kw)
        self.default_course = ICourse(self.context)


class SectionEditView(BaseEditView):
    """View for editing Sections."""

    __used_for__ = ISection

    @property
    def next_term(self):
        return getNextTerm(ITerm(self.context))

    @property
    def previous_term(self):
        return getPreviousTerm(ITerm(self.context))

    def update(self):
        if 'UNLINK_NEXT' in self.request:
            section = removeSecurityProxy(self.context)
            section.next = None
            return ''
        elif 'UNLINK_PREVIOUS' in self.request:
            section = removeSecurityProxy(self.context)
            section.previous = None
            return ''
        return BaseEditView.update(self)


class ConflictDisplayMixin(TimetableConflictMixin):
    """A mixin for use in views that display event conflicts."""

    def update(self):
        """Set self.busy_periods."""
        ttschema = self.getSchema()
        term = self.getTerm()
        if ttschema and term:
            section_map = self.sectionMap(term, ttschema)
            self.busy_periods = [(key, sections)
                                 for key, sections in section_map.items()
                                 if self.context in sections]
        else:
            self.busy_periods = []


class RelationshipEditConfView(RelationshipViewBase, ConflictDisplayMixin):
    """A relationship editing view that displays conflicts."""

    __call__ = ViewPageTemplateFile('templates/edit_relationships.pt')

    @property
    def term(self):
        return ITerm(self.context)

    @property
    def school_year(self):
        return ISchoolYear(self.context)

    def update(self):
        RelationshipViewBase.update(self)
        ConflictDisplayMixin.update(self)


class SectionInstructorView(RelationshipEditConfView, ConflictDisplayMixin):
    """View for adding instructors to a Section."""

    __used_for__ = ISection

    title = _("Instructors")
    current_title = _("Current Instructors")
    available_title = _("Available Instructors")

    def getCollection(self):
        return self.context.instructors

    def getAvailableItemsContainer(self):
        return ISchoolToolApplication(None)['persons']


class SectionLearnerView(RelationshipEditConfView):
    """View for adding learners to a Section.  """

    __used_for__ = ISection

    title = _("Students")
    current_title = _("Current Students")
    available_title = _("Available Students")

    def getCollection(self):
        return self.context.members

    def getSelectedItems(self):
        """Return a list of selected members."""
        return filter(IPerson.providedBy, self.getCollection())

    def getAvailableItemsContainer(self):
        return ISchoolToolApplication(None)['persons']
