import io
import tempfile
from collections import OrderedDict
from typing import Tuple

from defusedcsv import csv
from django import forms
from django.utils.translation import ugettext, ugettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES


class BaseExporter:
    """
    This is the base class for all data exporters
    """

    def __init__(self, event):
        self.event = event

    def __str__(self):
        return self.identifier

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this exporter. This should be short but
        self-explaining. Good examples include 'JSON' or 'Microsoft Excel'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this exporter.
        This should only contain lowercase letters and in most
        cases will be the same as your package name.
        """
        raise NotImplementedError()  # NOQA

    @property
    def export_form_fields(self) -> dict:
        """
        When the event's administrator visits the export page, this method
        is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be field names and
        the values should be corresponding Django form fields.

        We suggest that you return an ``OrderedDict`` object instead of a dictionary.
        Your implementation could look like this::

            @property
            def export_form_fields(self):
                return OrderedDict(
                    [
                        ('tab_width',
                         forms.IntegerField(
                             label=_('Tab width'),
                             default=4
                         ))
                    ]
                )
        """
        return {}

    def render(self, form_data: dict) -> Tuple[str, str, bytes]:
        """
        Render the exported file and return a tuple consisting of a filename, a file type
        and file content.

        :type form_data: dict
        :param form_data: The form data of the export details form
        :param output_file: You can optionally accept a parameter that will be given a file handle to write the
                            output to. In this case, you can return None instead of the file content.

        Note: If you use a ``ModelChoiceField`` (or a ``ModelMultipleChoiceField``), the
        ``form_data`` will not contain the model instance but only it's primary key (or
        a list of primary keys) for reasons of internal serialization when using background
        tasks.
        """
        raise NotImplementedError()  # NOQA


class ListExporter(BaseExporter):

    @property
    def export_form_fields(self) -> dict:
        ff = OrderedDict(
            [
                ('_format',
                 forms.ChoiceField(
                     label=_('Export format'),
                     choices=(
                         ('xlsx', _('Excel (.xlsx)')),
                         ('default', _('CSV (with commas)')),
                         ('csv-excel', _('CSV (Excel-style)')),
                         ('semicolon', _('CSV (with semicolons)')),
                     ),
                 )),
            ]
        )
        ff.update(self.additional_form_fields)
        return ff

    @property
    def additional_form_fields(self) -> dict:
        return {}

    def iterate_list(self, form_data):
        raise NotImplementedError()  # noqa

    def get_filename(self):
        return 'export.csv'

    def _render_csv(self, form_data, output_file=None, **kwargs):
        if output_file:
            writer = csv.writer(output_file, **kwargs)
            for line in self.iterate_list(form_data):
                writer.writerow(line)
            return self.get_filename() + '.csv', 'text/csv', None
        else:
            output = io.StringIO()
            writer = csv.writer(output, **kwargs)
            for line in self.iterate_list(form_data):
                writer.writerow(line)
            return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")

    def _render_xlsx(self, form_data, output_file=None):
        wb = Workbook()
        ws = wb.get_active_sheet()
        try:
            ws.title = str(self.verbose_name)
        except:
            pass
        for i, line in enumerate(self.iterate_list(form_data)):
            for j, val in enumerate(line):
                ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val

        if output_file:
            wb.save(output_file)
            return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
        else:
            with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
                wb.save(f.name)
                f.seek(0)
                return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()

    def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
        if form_data.get('_format') == 'xlsx':
            return self._render_xlsx(form_data, output_file=output_file)
        elif form_data.get('_format') == 'default':
            return self._render_csv(form_data, quoting=csv.QUOTE_NONNUMERIC, delimiter=',', output_file=output_file)
        elif form_data.get('_format') == 'csv-excel':
            return self._render_csv(form_data, dialect='excel', output_file=output_file)
        elif form_data.get('_format') == 'semicolon':
            return self._render_csv(form_data, dialect='excel', delimiter=';', output_file=output_file)


class MultiSheetListExporter(ListExporter):

    @property
    def sheets(self):
        raise NotImplementedError()

    @property
    def export_form_fields(self) -> dict:
        choices = [
            ('xlsx', _('Combined Excel (.xlsx)')),
        ]
        for s, l in self.sheets:
            choices += [
                (s + ':default', str(l) + ' – ' + ugettext('CSV (with commas)')),
                (s + ':excel', str(l) + ' – ' + ugettext('CSV (Excel-style)')),
                (s + ':semicolon', str(l) + ' – ' + ugettext('CSV (with semicolons)')),
            ]
        ff = OrderedDict(
            [
                ('_format',
                 forms.ChoiceField(
                     label=_('Export format'),
                     choices=choices,
                 )),
            ]
        )
        ff.update(self.additional_form_fields)
        return ff

    def iterate_list(self, form_data):
        pass

    def iterate_sheet(self, form_data, sheet):
        raise NotImplementedError()  # noqa

    def _render_sheet_csv(self, form_data, sheet, output_file=None, **kwargs):
        if output_file:
            writer = csv.writer(output_file, **kwargs)
            for line in self.iterate_sheet(form_data, sheet):
                writer.writerow(line)
            return self.get_filename() + '.csv', 'text/csv', None
        else:
            output = io.StringIO()
            writer = csv.writer(output, **kwargs)
            for line in self.iterate_sheet(form_data, sheet):
                writer.writerow(line)
            return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")

    def _render_xlsx(self, form_data, output_file=None):
        wb = Workbook()
        ws = wb.get_active_sheet()
        wb.remove(ws)
        for s, l in self.sheets:
            ws = wb.create_sheet(str(l))
            for i, line in enumerate(self.iterate_sheet(form_data, sheet=s)):
                for j, val in enumerate(line):
                    ws.cell(row=i + 1, column=j + 1).value = str(val) if not isinstance(val, KNOWN_TYPES) else val

        if output_file:
            wb.save(output_file)
            return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', None
        else:
            with tempfile.NamedTemporaryFile(suffix='.xlsx') as f:
                wb.save(f.name)
                f.seek(0)
                return self.get_filename() + '.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', f.read()

    def render(self, form_data: dict, output_file=None) -> Tuple[str, str, bytes]:
        if form_data.get('_format') == 'xlsx':
            return self._render_xlsx(form_data, output_file=output_file)
        elif ':' in form_data.get('_format'):
            sheet, f = form_data.get('_format').split(':')
            if f == 'default':
                return self._render_sheet_csv(form_data, sheet, quoting=csv.QUOTE_NONNUMERIC, delimiter=',',
                                              output_file=output_file)
            elif f == 'excel':
                return self._render_sheet_csv(form_data, sheet, dialect='excel', output_file=output_file)
            elif f == 'semicolon':
                return self._render_sheet_csv(form_data, sheet, dialect='excel', delimiter=';', output_file=output_file)
