"""
Base and abstract pages used in Django COS.
"""
# Standard Library Imports
import os
import json
import geocoder
# Django Imports
from django.db import models
from django.conf import settings
from django.utils import timezone
from django.http import JsonResponse
from django.utils.html import strip_tags
from django.core.paginator import Paginator
from django.shortcuts import render, redirect
from django.template import Context, Template
from django.template.loader import render_to_string
from django.core.mail import send_mail, EmailMessage
from django.utils.translation import ugettext_lazy as _
from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import MaxValueValidator, MinValueValidator
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
# Wagtail Imports
from wagtail.admin.edit_handlers import (
    HelpPanel,
    FieldPanel,
    FieldRowPanel,
    InlinePanel,
    MultiFieldPanel,
    ObjectList,
    PageChooserPanel,
    StreamFieldPanel,
    TabbedInterface)
from wagtail.core.fields import StreamField
from wagtail.core.models import Orderable, PageBase, Page, Site
from wagtail.core.utils import resolve_model_string
from wagtail.contrib.forms.edit_handlers import FormSubmissionsPanel
from wagtail.contrib.forms.forms import WagtailAdminFormPageForm
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtail.contrib.forms.models import FormSubmission
from wagtail.search import index
# Dependency Imports
from taggit.models import TaggedItemBase
from icalendar import Event as ICalEvent
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from eventtools.models import BaseEvent, BaseOccurrence
# Django COS Imports
from django_cos.blocks import (
    CONTENT_STREAMBLOCKS,
    LAYOUT_STREAMBLOCKS,
    ContentWallBlock,
    OpenHoursBlock,
    StructuredDataActionBlock)
from django_cos import schema, utils
from django_cos.fields import ColorField
from django_cos.settings import cr_settings
from django_cos.forms import DjangoCOSFormBuilder, DjangoCOSSubmissionsListView
from django_cos.models.wagtailsettings_models import GeneralSettings, LayoutSettings, SeoSettings, GoogleApiSettings

DJANGO_COS_PAGE_MODELS = []


def get_page_models():
    return DJANGO_COS_PAGE_MODELS

class DjangoCOSPageMeta(PageBase):
    def __init__(cls, name, bases, dct):
        super().__init__(name, bases, dct)
        if 'amp_template' not in dct:
            cls.amp_template = None
        if 'search_db_include' not in dct:
            cls.search_db_include = False
        if 'search_db_boost' not in dct:
            cls.search_db_boost = 0
        if 'search_filterable' not in dct:
            cls.search_filterable = False
        if 'search_name' not in dct:
            cls.search_name = cls._meta.verbose_name
        if 'search_name_plural' not in dct:
            cls.search_name_plural = cls._meta.verbose_name_plural
        if 'search_template' not in dct:
            cls.search_template = 'django_cos/pages/search_result.html'
        if not cls._meta.abstract:
            DJANGO_COS_PAGE_MODELS.append(cls)

class DjangoCOSTag(TaggedItemBase):
    class Meta:
        verbose_name = _('Django COS Tag')
    content_object = ParentalKey('django_cos.DjangoCOSPage', related_name='tagged_items')

class DjangoCOSPage(Page, metaclass=DjangoCOSPageMeta):
    """
    General use page with caching, templating, and SEO functionality.
    All pages should inherit from this.
    """
    class Meta:
        verbose_name = _('Django COS Page')
    # Do not allow this page type to be created in wagtail admin
    is_creatable = False

    # Templates
    # The page will render the following templates under certain conditions:
    #
    # template = ''
    # amp_template = ''
    # ajax_template = ''
    # search_template = ''

    ###############
    # Content fields
    ###############
    cover_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name=_('Cover image'),
    )

    ###############
    # Index fields
    ###############

    # Subclasses can override this to enabled index features by default.
    index_show_subpages_default = False

    # Subclasses can override this to query on a specific
    # page model, rather than the default wagtail Page.
    index_query_pagemodel = 'wagtailcore.Page'

    # Subclasses can override these fields to enable custom
    # ordering based on specific subpage fields.
    index_order_by_default = '-first_published_at'
    index_order_by_choices = (
        ('-first_published_at', _('Date first published, newest to oldest')),
        ('first_published_at', _('Date first published, oldest to newest')),
        ('-last_published_at', _('Date updated, newest to oldest')),
        ('last_published_at', _('Date updated, oldest to newest')),
        ('title', _('Title, alphabetical')),
        ('-title', _('Title, reverse alphabetical')),
    )
    index_show_subpages = models.BooleanField(
        default=index_show_subpages_default,
        verbose_name=_('Show list of child pages')
    )
    index_order_by = models.CharField(
        max_length=255,
        choices=index_order_by_choices,
        default=index_order_by_default,
        verbose_name=_('Order child pages by'),
    )
    index_num_per_page = models.PositiveIntegerField(
        default=10,
        verbose_name=_('Number per page'),
    )
    tags = ClusterTaggableManager(
        through=DjangoCOSTag,
        verbose_name='Tags',
        blank=True,
        help_text=_('Used to categorize your pages.')
    )

    ###############
    # Layout fields
    ###############

    custom_template = models.CharField(
        blank=True,
        max_length=255,
        choices=None,
        verbose_name=_('Template')
    )


    ###############
    # SEO fields
    ###############

    og_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name=_('Open Graph preview image'),
        help_text=_('The image shown when linking to this page on social media. If blank, defaults to article cover image, or logo in Settings > Layout > Logo')
    )
    struct_org_type = models.CharField(
        default='',
        blank=True,
        max_length=255,
        choices=schema.SCHEMA_ORG_CHOICES,
        verbose_name=_('Organization type'),
        help_text=_('If blank, no structured data will be used on this page.')
    )
    struct_org_name = models.CharField(
        default='',
        blank=True,
        max_length=255,
        verbose_name=_('Organization name'),
        help_text=_('Leave blank to use the site name in Settings > Sites')
    )
    struct_org_logo = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name=_('Organization logo'),
        help_text=_('Leave blank to use the logo in Settings > Layout > Logo')
    )
    struct_org_image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name=_('Photo of Organization'),
        help_text=_('A photo of the facility. This photo will be cropped to 1:1, 4:3, and 16:9 aspect ratios automatically.')
    )
    struct_org_phone = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_('Telephone number'),
        help_text=_('Include country code for best results. For example: +1-216-555-8000')
    )
    struct_org_address_street = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_('Street address'),
        help_text=_('House number and street. For example, 55 Public Square Suite 1710')
    )
    struct_org_address_locality = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_('City'),
        help_text=_('City or locality. For example, Cleveland')
    )
    struct_org_address_region = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_('State'),
        help_text=_('State, province, county, or region. For example, OH')
    )
    struct_org_address_postal = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_('Postal code'),
        help_text=_('Zip or postal code. For example, 44113')
    )
    struct_org_address_country = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_('Country'),
        help_text=_('For example, USA. Two-letter ISO 3166-1 alpha-2 country code is also acceptible https://en.wikipedia.org/wiki/ISO_3166-1')
    )
    struct_org_geo_lat = models.DecimalField(
        blank=True,
        null=True,
        max_digits=10,
        decimal_places=8,
        verbose_name=_('Geographic latitude')
    )
    struct_org_geo_lng = models.DecimalField(
        blank=True,
        null=True,
        max_digits=10,
        decimal_places=8,
        verbose_name=_('Geographic longitude')
    )
    struct_org_hours = StreamField(
        [
            ('hours', OpenHoursBlock()),
        ],
        blank=True,
        verbose_name=_('Hours of operation')
    )
    struct_org_actions = StreamField(
        [
            ('actions', StructuredDataActionBlock())
        ],
        blank=True,
        verbose_name=_('Actions')
    )
    struct_org_extra_json = models.TextField(
        blank=True,
        verbose_name=_('Additional Organization markup'),
        help_text=_('Additional JSON-LD inserted into the Organization dictionary. Must be properties of https://schema.org/Organization or the selected organization type.')
    )


    ###############
    # Settings
    ###############

    content_walls = StreamField(
        [
            ('content_wall', ContentWallBlock())
        ],
        blank=True,
        verbose_name=_('Content Walls')
    )

    ###############
    # Search
    ###############

    search_fields = [
        index.SearchField('title', partial_match=True, boost=3),
        index.SearchField('seo_title', partial_match=True, boost=3),
        index.SearchField('search_description', boost=2),
        index.FilterField('title'),
        index.FilterField('id'),
        index.FilterField('live'),
        index.FilterField('owner'),
        index.FilterField('content_type'),
        index.FilterField('path'),
        index.FilterField('depth'),
        index.FilterField('locked'),
        index.FilterField('first_published_at'),
        index.FilterField('last_published_at'),
        index.FilterField('latest_revision_created_at'),
        index.FilterField('index_show_subpages'),
        index.FilterField('index_order_by'),
        index.FilterField('custom_template'),
    ]

    ###############
    # Panels
    ###############

    content_panels = Page.content_panels + [
        ImageChooserPanel('cover_image'),
    ]

    body_content_panels = []

    bottom_content_panels = [
        FieldPanel('tags'),
    ]

    layout_panels = [
        MultiFieldPanel(
            [
                FieldPanel('custom_template')
            ],
            heading=_('Visual Design')
        ),
        MultiFieldPanel(
            [
                FieldPanel('index_show_subpages'),
                FieldPanel('index_num_per_page'),
                FieldPanel('index_order_by'),
            ],
            heading=_('Show Child Pages')
        )
    ]

    promote_panels = [
        MultiFieldPanel(
            [
                FieldPanel('slug'),
                FieldPanel('seo_title'),
                FieldPanel('search_description'),
                ImageChooserPanel('og_image'),
            ],
            _('Page Meta Data')
        ),
        MultiFieldPanel(
            [
                HelpPanel(
                    heading=_('About Organization Structured Data'),
                    content=_("""The fields below help define brand, contact, and storefront
                    information to search engines. This information should be filled out on
                    the site’s root page (Home Page). If your organization has multiple locations,
                    then also fill this info out on each location page using that particular
                    location’s info."""),
                ),
                FieldPanel('struct_org_type'),
                FieldPanel('struct_org_name'),
                ImageChooserPanel('struct_org_logo'),
                ImageChooserPanel('struct_org_image'),
                FieldPanel('struct_org_phone'),
                FieldPanel('struct_org_address_street'),
                FieldPanel('struct_org_address_locality'),
                FieldPanel('struct_org_address_region'),
                FieldPanel('struct_org_address_postal'),
                FieldPanel('struct_org_address_country'),
                FieldPanel('struct_org_geo_lat'),
                FieldPanel('struct_org_geo_lng'),
                StreamFieldPanel('struct_org_hours'),
                StreamFieldPanel('struct_org_actions'),
                FieldPanel('struct_org_extra_json'),
            ],
            _('Structured Data - Organization')
        ),
    ]

    settings_panels = Page.settings_panels + [
        StreamFieldPanel('content_walls'),
    ]

    def __init__(self, *args, **kwargs):
        """
        Inject custom choices and defalts into the form fields
        to enable customization by subclasses.
        """
        super().__init__(*args, **kwargs)

        klassname = self.__class__.__name__.lower()
        template_choices = cr_settings['FRONTEND_TEMPLATES_PAGES'].get('*', ()) + \
                           cr_settings['FRONTEND_TEMPLATES_PAGES'].get(klassname, ())


        self._meta.get_field('index_order_by').choices = self.index_order_by_choices
        self._meta.get_field('custom_template').choices = template_choices
        if not self.id:
            self.index_order_by = self.index_order_by_default
            self.index_show_subpages = self.index_show_subpages_default

    @classmethod
    def get_edit_handler(cls):
        """
        Override to "lazy load" the panels overriden by subclasses.
        """
        return TabbedInterface([
            ObjectList(cls.content_panels + cls.body_content_panels + cls.bottom_content_panels, heading='Content'),
            ObjectList(cls.layout_panels, heading='Layout'),
            ObjectList(cls.promote_panels, heading='SEO', classname="seo"),
            ObjectList(cls.settings_panels, heading='Settings', classname="settings"),
        ]).bind_to_model(cls)

    def get_struct_org_name(self):
        """
        Gets org name for sturctured data using a fallback.
        """
        if self.struct_org_name:
            return self.struct_org_name
        return self.get_site().site_name

    def get_struct_org_logo(self):
        """
        Gets logo for structured data using a fallback.
        """
        if self.struct_org_logo:
            return self.struct_org_logo
        else:
            layout_settings = LayoutSettings.for_site(self.get_site())
            if layout_settings.logo:
                return layout_settings.logo
        return None

    def get_template(self, request, *args, **kwargs):
        """
        Override parent to serve different templates based on querystring.
        """
        if 'amp' in request.GET and hasattr(self, 'amp_template'):
            seo_settings = SeoSettings.for_site(request.site)
            if seo_settings.amp_pages:
                if request.is_ajax():
                    return self.ajax_template or self.amp_template
                return self.amp_template

        if self.custom_template:
            return self.custom_template

        return super(DjangoCOSPage, self).get_template(request, args, kwargs)

    def get_index_children(self):
        """
        Override to return query of subpages as defined by `index_` variables.
        """
        if self.index_query_pagemodel:
            querymodel = resolve_model_string(self.index_query_pagemodel, self._meta.app_label)
            return querymodel.objects.child_of(self).order_by(self.index_order_by)

        return super().get_children().live()

    def get_content_walls(self, check_child_setting=True):
        current_content_walls = []
        if check_child_setting:
            for wall in self.content_walls:
                content_wall = wall.value
                if wall.value['show_content_wall_on_children']:
                    current_content_walls.append(wall.value)
        else:
            current_content_walls = self.content_walls

        try:
            return list(current_content_walls) + self.get_parent().specific.get_content_walls()
        except AttributeError:
            return list(current_content_walls)

    def get_context(self, request, *args, **kwargs):
        """
        Add child pages and paginated child pages to context.
        """
        context = super().get_context(request)

        if self.index_show_subpages:
            all_children = self.get_index_children()
            paginator = Paginator(all_children, self.index_num_per_page)
            page = request.GET.get('p', 1)
            try:
                paged_children = paginator.page(page)
            except:
                paged_children = paginator.page(1)

            context['index_paginated'] = paged_children
            context['index_children'] = all_children
        context['content_walls'] = self.get_content_walls(check_child_setting=False)
        return context




###############################################################################
# Abstract pages providing pre-built common website functionality, suitable for subclassing.
# These are abstract so subclasses can override fields if desired.
###############################################################################


class DjangoCOSWebPage(DjangoCOSPage):
    """
    Provides a body and body-related functionality.
    This is abstract so that subclasses can override the body StreamField.
    """
    class Meta:
        verbose_name = _('Django COS Web Page')
        abstract = True

    template = 'django_cos/pages/web_page.html'

    # Child pages should override based on what blocks they want in the body.
    # Default is LAYOUT_STREAMBLOCKS which is the fullest editor experience.
    body = StreamField(LAYOUT_STREAMBLOCKS, null=True, blank=True)

    # Search fields
    search_fields = (
        DjangoCOSPage.search_fields +
        [index.SearchField('body')]
    )

    # Panels
    body_content_panels = [
        StreamFieldPanel('body'),
    ]

    @property
    def body_preview(self):
        """
        A shortened, non-HTML version of the body.
        """
        # add spaces between tags for legibility
        body = str(self.body).replace('>', '> ')
        # strip tags
        body = strip_tags(body)
        # truncate and add ellipses

        return body[:200] + "..." if len(body) > 200 else body

    @property
    def page_ptr(self):
        """
        Overwrite of `page_ptr` to make it compatible with wagtailimportexport.
        """
        return self.base_page_ptr

    @page_ptr.setter
    def page_ptr(self, value):
        self.base_page_ptr = value


class DjangoCOSArticlePage(DjangoCOSWebPage):
    """
    Article, suitable for news or blog content.
    """
    class Meta:
        verbose_name = _('Django COS Article')
        abstract = True

    template = 'django_cos/pages/article_page.html'
    amp_template = 'django_cos/pages/article_page.amp.html'

    # Override body to provide simpler content
    body = StreamField(CONTENT_STREAMBLOCKS, null=True, blank=True)

    caption = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Caption'),
    )
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        editable=True,
        on_delete=models.SET_NULL,
        verbose_name=_('Author'),
    )
    author_display = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Display author as'),
        help_text=_('Override how the author’s name displays on this article.'),
    )
    date_display = models.DateField(
        null=True,
        blank=True,
        verbose_name=_('Display publish date'),
    )

    def get_author_name(self):
        """
        Gets author name using a fallback.
        """
        if self.author_display:
            return self.author_display
        if self.author:
            return self.author.get_full_name()
        return ''

    def get_pub_date(self):
        """
        Gets published date.
        """
        if self.date_display:
            return self.date_display
        return ''

    def get_description(self):
        """
        Gets the description using a fallback.
        """
        if self.search_description:
            return self.search_description
        if self.caption:
            return self.caption
        if self.body_preview:
            return self.body_preview
        return ''

    search_fields = (
        DjangoCOSWebPage.search_fields +
        [
            index.SearchField('caption', boost=2),
            index.FilterField('author'),
            index.FilterField('author_display'),
            index.FilterField('date_display'),
        ]
    )

    content_panels = DjangoCOSWebPage.content_panels + [
        FieldPanel('caption'),
        MultiFieldPanel(
            [
                FieldPanel('author'),
                FieldPanel('author_display'),
                FieldPanel('date_display'),
            ],
            _('Publication Info')
        )
    ]


class DjangoCOSArticleIndexPage(DjangoCOSWebPage):
    """
    Shows a list of article sub-pages.
    """
    class Meta:
        verbose_name = _('Django COS Article Index Page')
        abstract = True

    template = 'django_cos/pages/article_index_page.html'

    index_show_subpages_default = True

    index_order_by_default = '-date_display'
    index_order_by_choices = (('-date_display', 'Display publish date, newest first'),) + \
        DjangoCOSWebPage.index_order_by_choices

    show_images = models.BooleanField(
        default=True,
        verbose_name=_('Show images'),
    )
    show_captions = models.BooleanField(
        default=True,
    )
    show_meta = models.BooleanField(
        default=True,
        verbose_name=_('Show author and date info'),
    )
    show_preview_text = models.BooleanField(
        default=True,
        verbose_name=_('Show preview text'),
    )

    layout_panels = DjangoCOSWebPage.layout_panels + [
        MultiFieldPanel(
            [
                FieldPanel('show_images'),
                FieldPanel('show_captions'),
                FieldPanel('show_meta'),
                FieldPanel('show_preview_text'),
            ],
            heading=_('Child page display')
        ),
    ]


class DjangoCOSEventPage(DjangoCOSWebPage, BaseEvent):
    class Meta:
        verbose_name = _('Django COS Event')
        abstract = True

    calendar_color = ColorField(
        blank=True,
        help_text=_('The color that the event will use when displayed on a calendar.'),
    )
    address = models.TextField(
        blank=True,
        verbose_name=_("Address")
    )
    content_panels = DjangoCOSWebPage.content_panels + [
        MultiFieldPanel(
            [
                FieldPanel('calendar_color'),
                FieldPanel('address'),
            ],
            heading=_('Event information')
        ),
        InlinePanel(
            'occurrences',
            min_num=1,
            heading=_("Dates and times"),
        ),
    ]

    @property
    def upcoming_occurrences(self):
        """
        Returns the next x occurrences for this event.
        By default, it returns 10.
        """
        return self.query_occurrences(num_of_instances_to_return=10)

    @property
    def most_recent_occurrence(self):
        """
        Gets the next upcoming, or last occurrence if the event has no more occurrences.
        """
        noc = self.next_occurrence()
        if noc:
            return noc
        else:
            aoc = []
            for occurrence in self.occurrences.all():
                aoc += [instance for instance in occurrence.all_occurrences()]
            if len(aoc) > 0:
                return aoc[-1] # last one in the list
            return None

    def query_occurrences(self, num_of_instances_to_return=None, **kwargs):
        """
        Returns a list of all upcoming event instances for the specified query.
        For more information on what you can query with, visit
        https://github.com/gregplaysguitar/django-eventtools
        """
        event_instances = []
        occurrence_kwargs = {
            'from_date': kwargs.get('from_date', timezone.now().date())
        }

        if 'limit' in kwargs:
            if kwargs['limit'] != None:
                # Limit the number of event instances that will be generated per occurrence rule to 10, if not otherwise specified.
                occurrence_kwargs['limit'] = kwargs.get('limit', 10)

        # For each occurrence rule in all of the occurrence rules for this event.
        for occurrence in self.occurrences.all():

            # Add the qualifying generated event instances to the list.
            event_instances += [instance for instance in occurrence.all_occurrences(**occurrence_kwargs)]

        # Sort all the events by the date that they start
        event_instances.sort(key=lambda d: d[0])

        # Return the event instances, possibly spliced if num_instances_to_return is set.
        return event_instances[:num_of_instances_to_return] if num_of_instances_to_return else event_instances

    def convert_to_ical_format(self, dt_start=None, dt_end=None, occurrence=None):
        ical_event = ICalEvent()
        ical_event.add('summary', self.title)
        if self.address:
            ical_event.add('location', self.address)

        if dt_start:
            ical_event.add('dtstart', dt_start)

            if dt_end:
                ical_event.add('dtend', dt_end)

        if occurrence:
            freq = occurrence.repeat.split(":")[1] if occurrence.repeat else None
            repeat_until = occurrence.repeat_until.strftime("%Y%m%dT000000Z") if occurrence.repeat_until else None

            ical_event.add('dtstart', occurrence.start)

            if occurrence.end:
                ical_event.add('dtend', occurrence.end)

            if freq:
                ical_event.add('RRULE', freq, encode=False)

            if repeat_until:
                ical_event.add('until', repeat_until)

        return ical_event

    def create_single_ical(self, dt_start, dt_end=None):
        return self.convert_to_ical_format(dt_start=dt_start, dt_end=dt_end)

    def create_recurring_ical(self):
        events = []
        for occurrence in self.occurrences.all():
            events.append(self.convert_to_ical_format(occurrence=occurrence))
        return events

class DefaultCalendarViewChoices():
    MONTH = 'month'
    AGENDA_WEEK = 'agendaWeek'
    AGENDA_DAY = 'agendaDay'
    LIST_MONTH = 'listMonth'

    CHOICES = (
            ('', _('No calendar')),
            (MONTH, _('Monthly Calendar')),
            (AGENDA_WEEK, _('Weekly Calendar')),
            (AGENDA_DAY, _('Daily Calendar')),
            (LIST_MONTH, _('Calendar List View')),
        )

class DjangoCOSEventIndexPage(DjangoCOSWebPage):
    """
    Shows a list of event sub-pages.
    """
    class Meta:
        verbose_name = _('Django COS Event Index Page')
        abstract = True

    template = 'django_cos/pages/event_index_page.html'

    index_show_subpages_default = True

    index_order_by_default = 'next_occurrence'
    index_order_by_choices = (
            ('next_occurrence', 'Display next occurrence, soonest first'),
        ) + \
        DjangoCOSWebPage.index_order_by_choices

    default_calendar_view = models.CharField(
        blank=True,
        choices=DefaultCalendarViewChoices.CHOICES,
        max_length=255,
        verbose_name=_('Calendar Style'),
        help_text=_('The default look of the calendar on this page.')
    )

    layout_panels = DjangoCOSWebPage.layout_panels + [
        FieldPanel('default_calendar_view'),
    ]

    def get_index_children(self):
        if self.index_query_pagemodel and self.index_order_by == 'next_occurrence':
            querymodel = resolve_model_string(self.index_query_pagemodel, self._meta.app_label)
            qs = querymodel.objects.child_of(self).live()
            # filter out events that don't have a next_occurrence
            upcoming = []
            for event in qs.all():
                if event.next_occurrence():
                    upcoming.append(event)
            # sort the events by next_occurrence
            return sorted(upcoming, key=lambda e: e.next_occurrence())

        return super().get_index_children()

    def get_calendar_events(self, start, end):
        # start with all child events, regardless of get_index_children rules.
        querymodel = resolve_model_string(self.index_query_pagemodel, self._meta.app_label)
        qs = querymodel.objects.child_of(self).live()
        event_instances = []
        for event in qs:
            occurrences = event.query_occurrences(limit=None, from_date=start, to_date=end)
            for occurrence in occurrences:
                event_data = {
                    'title': event.title,
                    'start': occurrence[0].strftime('%Y-%m-%dT%H:%M:%S'),
                    'end' : occurrence[1].strftime('%Y-%m-%dT%H:%M:%S') if occurrence[1] else "",
                    'description': "",
                }
                if event.url:
                    event_data['url'] = event.url
                if event.calendar_color:
                    event_data['backgroundColor'] = event.calendar_color
                event_instances.append(event_data)
        return event_instances


class DjangoCOSEventOccurrence(Orderable, BaseOccurrence):
    class Meta:
        verbose_name = _('Django COS Event Occurrence')
        abstract = True


class DjangoCOSFormPage(DjangoCOSWebPage):
    """
    This is basically a clone of wagtail.contrib.forms.models.AbstractForm
    with changes in functionality and extending DjangoCOSWebPage vs wagtailcore.Page.
    """
    class Meta:
        verbose_name = _('Django COS Form Page')
        abstract = True

    template = 'django_cos/pages/form_page.html'
    landing_page_template = 'django_cos/pages/form_page_landing.html'

    base_form_class = WagtailAdminFormPageForm

    form_builder = DjangoCOSFormBuilder

    submissions_list_view_class = DjangoCOSSubmissionsListView

    ### Custom django cos fields

    to_address = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Email form submissions to'),
        help_text=_('Optional - email form submissions to this address. Separate multiple addresses by comma.')
    )
    subject = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Subject'),
    )
    save_to_database = models.BooleanField(
        default=True,
        verbose_name=_('Save form submissions'),
        help_text=_('Submissions are saved to database and can be exported at any time.')
    )
    thank_you_page = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name=_('Thank you page'),
        help_text=_('The page users are redirected to after submitting the form.'),
    )
    button_text = models.CharField(
        max_length=255,
        default=_('Submit'),
        verbose_name=_('Button text'),
    )
    button_style = models.CharField(
        blank=True,
        choices=cr_settings['FRONTEND_BTN_STYLE_CHOICES'],
        default=cr_settings["FRONTEND_BTN_STYLE_DEFAULT"],
        max_length=255,
        verbose_name=_('Button style'),
    )
    button_size = models.CharField(
        blank=True,
        choices=cr_settings['FRONTEND_BTN_SIZE_CHOICES'],
        default=cr_settings["FRONTEND_BTN_SIZE_DEFAULT"],
        max_length=255,
        verbose_name=_('Button Size'),
    )
    button_css_class = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Button CSS class'),
        help_text=_('Custom CSS class applied to the submit button.'),
    )
    form_css_class = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Form CSS Class'),
        help_text=_('Custom CSS class applied to <form> element.'),
    )
    form_id = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('Form ID'),
        help_text=_('Custom ID applied to <form> element.'),
    )
    form_golive_at = models.DateTimeField(
        blank=True,
        null=True,
        verbose_name=_('Form go live date/time'),
        help_text=_('Date and time when the FORM goes live on the page.'),
    )
    form_expire_at = models.DateTimeField(
        blank=True,
        null=True,
        verbose_name=_('Form expiry date/time'),
        help_text=_('Date and time when the FORM will no longer be available on the page.'),
    )

    body_content_panels = DjangoCOSWebPage.body_content_panels + [
        FormSubmissionsPanel(),
        InlinePanel('form_fields', label="Form fields"),
        MultiFieldPanel(
            [
                PageChooserPanel('thank_you_page'),
                FieldPanel('button_text'),
                FieldPanel('button_style'),
                FieldPanel('button_size'),
                FieldPanel('button_css_class'),
                FieldPanel('form_css_class'),
                FieldPanel('form_id'),
            ],
            _('Form Settings')
        ),
        MultiFieldPanel(
            [
                FieldPanel('save_to_database'),
                FieldPanel('to_address'),
                FieldPanel('subject'),
            ],
            _('Form Submissions')
        ),
        InlinePanel('confirmation_emails', label=_('Confirmation Emails'))
    ]

    settings_panels = DjangoCOSPage.settings_panels + [
        MultiFieldPanel(
            [
                FieldRowPanel(
                    [
                        FieldPanel('form_golive_at'),
                        FieldPanel('form_expire_at'),
                    ],
                    classname='label-above',
                ),
            ],
            _('Form Scheduled Publishing'),
        )
    ]

    @property
    def form_live(self):
        """
        A boolean on whether or not the <form> element should be shown on the page.
        """
        return (self.form_golive_at is None or self.form_golive_at <= timezone.now()) and \
               (self.form_expire_at is None or self.form_expire_at >= timezone.now())


    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not hasattr(self, 'landing_page_template'):
            name, ext = os.path.splitext(self.template)
            self.landing_page_template = name + '_landing' + ext

    def get_form_fields(self):
        """
        Form page expects `form_fields` to be declared.
        If you want to change backwards relation name,
        you need to override this method.
        """

        return self.form_fields.all()

    def get_data_fields(self):
        """
        Returns a list of tuples with (field_name, field_label).
        """

        data_fields = [
            ('submit_time', _('Submission date')),
        ]
        data_fields += [
            (field.clean_name, field.label)
            for field in self.get_form_fields()
        ]

        return data_fields

    def get_form_class(self):
        fb = self.form_builder(self.get_form_fields())
        return fb.get_form_class()

    def get_form_parameters(self):
        return {}

    def get_form(self, *args, **kwargs):
        form_class = self.get_form_class()
        form_params = self.get_form_parameters()
        form_params.update(kwargs)

        return form_class(*args, **form_params)

    def get_landing_page_template(self, request, *args, **kwargs):
        return self.landing_page_template

    def get_submission_class(self):
        """
        Returns submission class.

        You can override this method to provide custom submission class.
        Your class must be inherited from AbstractFormSubmission.
        """

        return FormSubmission

    def process_form_submission(self, request, form):
        """
        Accepts form instance with submitted data, user and page.
        Creates submission instance.

        You can override this method if you want to have custom creation logic.
        For example, if you want to save reference to a user.
        """
        processed_data = {}

        # Handle file uploads
        for key, val in form.cleaned_data.items():
            if type(val) == InMemoryUploadedFile or type(val) == TemporaryUploadedFile:
                # Save the file and get its URL
                file_system = FileSystemStorage(
                    location=cr_settings['PROTECTED_MEDIA_ROOT'],
                    base_url=cr_settings['PROTECTED_MEDIA_URL']
                )
                filename = file_system.save(file_system.get_valid_name(val.name), val)
                processed_data[key] = file_system.url(filename)
            else:
                processed_data[key] = val

        # Get submission
        form_submission = self.get_submission_class()(
            form_data=json.dumps(processed_data, cls=DjangoJSONEncoder),
            page=self,
        )

        # Save to database
        if self.save_to_database:
            form_submission.save()

        # Send the mails
        if self.to_address:
            self.send_summary_mail(request, form, processed_data)

        if self.confirmation_emails:
            for email in self.confirmation_emails.all():
                from_address = email.from_address

                if from_address == '':
                    from_address = GeneralSettings.for_site(request.site).from_email_address

                template_body = Template(email.body)
                template_to = Template(email.to_address)
                template_from_email = Template(from_address)
                template_cc = Template(email.cc_address)
                template_bcc = Template(email.bcc_address)
                template_subject = Template(email.subject)
                context = Context(self.data_to_dict(processed_data))

                message = EmailMessage(
                    body=template_body.render(context),
                    to=template_to.render(context).split(','),
                    from_email=template_from_email.render(context),
                    cc=template_cc.render(context).split(','),
                    bcc=template_bcc.render(context).split(','),
                    subject=template_subject.render(context),
                )

                message.content_subtype = 'html'
                message.send()

        return processed_data

    def send_summary_mail(self, request, form, processed_data):
        """
        Sends a form submission summary email.
        """
        addresses = [x.strip() for x in self.to_address.split(',')]
        content = []
        for field in form:
            value = processed_data[field.name]
            if isinstance(value, list):
                value = ', '.join(value)
            content.append('{0}: {1}'.format(field.label, utils.attempt_protected_media_value_conversion(request, value)))
        content = '\n'.join(content)
        send_mail(
            self.subject,
            content,
            GeneralSettings.for_site(Site.objects.get(is_default_site=True)).from_email_address,
            addresses
        )

    def data_to_dict(self, processed_data):
        """
        Converts processed form data into a dictionary suitable
        for rendering in a context.
        """
        dictionary = {}

        for key, value in processed_data.items():
            dictionary[key.replace('-', '_')] = value
            if isinstance(value, list):
                dictionary[key] = ', '.join(value)

        return dictionary

    def render_landing_page(self, request, *args, form_submission=None, **kwargs):
        """
        Renders the landing page.

        You can override this method to return a different HttpResponse as
        landing page. E.g. you could return a redirect to a separate page.
        """
        if self.thank_you_page:
            return redirect(self.thank_you_page.url)

        context = self.get_context(request)
        context['form_submission'] = form_submission
        return render(
            request,
            self.get_landing_page_template(request),
            context
        )

    def serve_submissions_list_view(self, request, *args, **kwargs):
        """
        Returns list submissions view for admin.

        `list_submissions_view_class` can bse set to provide custom view class.
        Your class must be inherited from SubmissionsListView.
        """
        view = self.submissions_list_view_class.as_view()
        return view(request, form_page=self, *args, **kwargs)

    def serve(self, request, *args, **kwargs):
        if request.method == 'POST':
            form = self.get_form(request.POST, request.FILES, page=self, user=request.user)

            if form.is_valid():
                form_submission = self.process_form_submission(request, form)
                return self.render_landing_page(request, form_submission, *args, **kwargs)
        else:
            form = self.get_form(page=self, user=request.user)

        context = self.get_context(request)
        context['form'] = form
        return render(
            request,
            self.get_template(request),
            context
        )

    preview_modes = [
        ('form', _('Form')),
        ('landing', _('Thank you page')),
    ]

    def serve_preview(self, request, mode):
        if mode == 'landing':
            request.is_preview = True
            return self.render_landing_page(request)

        return super().serve_preview(request, mode)


class DjangoCOSLocationPage(DjangoCOSWebPage):
    """
    Location, suitable for store locations or help centers.
    """
    class Meta:
        verbose_name = _('Django COS Location')
        abstract = True

    template = 'django_cos/pages/location_page.html'

    # Override body to provide simpler content
    body = StreamField(CONTENT_STREAMBLOCKS, null=True, blank=True)

    address = models.TextField(
        blank=True,
        verbose_name=_("Address")
    )
    latitude = models.FloatField(
        blank=True,
        null=True,
        verbose_name=_("Latitude")
    )
    longitude = models.FloatField(
        blank=True,
        null=True,
        verbose_name=_("Longitude")
    )
    auto_update_latlng = models.BooleanField(
        default=True,
        verbose_name=_("Auto Update Latitude and Longitude"),
        help_text=_("If checked, automatically update the latitude and longitude when the address is updated.")
    )
    map_title = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_("Map Title"),
        help_text=_("If this is filled out, this is the title that will be used on the map.")
    )
    map_description = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_("Map Description"),
        help_text=_("If this is filled out, this is the description that will be used on the map.")
    )
    website = models.TextField(
        blank=True,
        verbose_name=_("Website")
    )
    phone_number = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_("Phone Number")
    )

    content_panels = DjangoCOSWebPage.content_panels + [
        FieldPanel('address'),
        FieldPanel('website'),
        FieldPanel('phone_number'),
    ]

    layout_panels = DjangoCOSWebPage.layout_panels + [
        MultiFieldPanel(
            [
                FieldPanel('map_title'),
                FieldPanel('map_description'),
            ],
            heading=_('Map Layout')
        ),
    ]

    settings_panels = DjangoCOSWebPage.settings_panels + [
        MultiFieldPanel(
            [
                FieldPanel('auto_update_latlng'),
                FieldPanel('latitude'),
                FieldPanel('longitude'),
            ],
            heading=_("Location Settings")
        ),
    ]

    @property
    def geojson_name(self):
        return self.map_title or self.title

    @property
    def geojson_description(self):
        return self.map_description

    @property
    def render_pin_description(self):
        return render_to_string(
            'django_cos/includes/map_pin_description.html',
            {
                'page': self
            }
        )

    @property
    def render_list_description(self):
        return render_to_string(
            'django_cos/includes/map_list_description.html',
            {
                'page': self
            }
        )

    def to_geojson(self):
        return {
            "type": "Feature",
            "geometry":{
                "type": "Point",
                "coordinates": [self.longitude, self.latitude]
            },
            "properties":{
                "list_description": self.render_list_description,
                "pin_description": self.render_pin_description
            }
        }

    def save(self, *args, **kwargs):
        if self.auto_update_latlng and GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key:
            try:
                g = geocoder.google(self.address, key=GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key)
                self.latitude = g.latlng[0]
                self.longitude = g.latlng[1]
            except TypeError:
                """Raised if google denied the request"""
                pass

        return super(DjangoCOSLocationPage, self).save(*args, **kwargs)


    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request)
        context['google_api_key'] = GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key
        return context


class DjangoCOSLocationIndexPage(DjangoCOSWebPage):
    """
    Shows a map view of the children DjangoCOSLocationPage.
    """
    class Meta:
        verbose_name = _('Django COS Location Index Page')
        abstract = True

    template = 'django_cos/pages/location_index_page.html'

    index_show_subpages_default = True

    center_latitude = models.FloatField(
        null=True,
        blank=True,
        help_text=_('The default latitude you want the map set to.'),
        default=0
    )
    center_longitude = models.FloatField(
        null=True,
        blank=True,
        help_text=_('The default longitude you want the map set to.'),
        default=0
    )
    zoom = models.IntegerField(
        default=8,
        validators=[
            MaxValueValidator(20),
            MinValueValidator(1),
        ],
        help_text=_('Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings')
    )

    layout_panels = DjangoCOSWebPage.layout_panels + [
        MultiFieldPanel(
            [
                FieldPanel('center_latitude'),
                FieldPanel('center_longitude'),
                FieldPanel('zoom'),
            ],
            heading=_('Map Display')
        ),
    ]

    def geojson_data(self, viewport=None):
        """
        function that will return all locations under this index as geoJSON compliant data.
        It is filtered by a latitude/longitude viewport if given.

        viewport is a string in the format of :
        'southwest.latitude,southwest.longitude|northeast.latitude,northeast.longitude'

        An example viewport that covers Cleveland, OH would look like this:
        '41.354912150983964,-81.95331736661791|41.663427748126935,-81.45206614591478'
        """
        qs = self.get_index_children().live()

        if viewport:
            southwest, northeast = viewport.split('|')
            southwest = [float(x) for x in southwest.split(',')]
            northeast = [float(x) for x in northeast.split(',')]

            qs = qs.filter(latitude__gte=southwest[0], latitude__lte=northeast[0], longitude__gte=southwest[1], longitude__lte=northeast[1])

        return {
            "type": "FeatureCollection",
            "features": [
                location.to_geojson() for location in qs
            ]
        }

    def serve(self, request, *args, **kwargs):
        data_format = request.GET.get('data-format', None)
        if data_format == 'geojson':
            return self.serve_geojson(request, *args, **kwargs)
        return super().serve(request, *args, **kwargs)

    def serve_geojson(self, request, *args, **kwargs):
        viewport = request.GET.get('viewport', None)
        return JsonResponse(self.geojson_data(viewport=viewport))

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request)
        context['google_api_key'] = GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key
        return context
