"""This module holds the reader/writer XLS of the ``import_export`` app."""
from django.db.models import QuerySet

from openpyxl import Workbook, load_workbook
from openpyxl.cell import get_column_letter
from openpyxl.comments import Comment
from openpyxl.worksheet.datavalidation import DataValidation

from .stylexlsx import StyleXlsx


class Row(dict):
    """Represent a row in xlsx file.

    A Row instance is a dictionary so it can store data with key/value system.
    Moreover, we can add an attribute `line` to keep the line number of data.

    Attributes
    ----------
    line : int or str
        Line number of data.
    """

    line = None

    def __init__(self, *args, **kwargs):
        if 'line' in kwargs:
            self.line = kwargs.pop('line')
        super().__init__(*args, **kwargs)


class BaseXLS:
    """ Class contains global method used for import/export XLS files.

    Attributes
    ----------
    workbook : openpyxl.Workbook
        Workbook which be used for process.
    nrows : dict
        Collection of current line number.
    """

    STYLE = None

    def __init__(self, *args, **kwargs):
        self.workbook = None
        self.nrows = {}
        self.STYLE = StyleXlsx()

    @property
    def first_sheet_name(self):
        """ Return the first sheet name of current workbook.

        Returns
        -------
        string
            Name of the first sheet
        """
        first_sheet_name = self.workbook.get_sheet_names()[0]
        return first_sheet_name

    @property
    def first_sheet(self):
        """ Return the first sheet of current workbook.

        Returns
        -------
        string
            The first sheet
        """
        sheet_name = self.first_sheet_name
        return self.workbook[sheet_name]

    def jump_to_next_row(self, sheet_name):
        """ Jump to next row in sheet.

        Parameters
        ----------
        sheet_name : string
            Name of sheet
        """
        self.nrows[sheet_name] += 1


class XLSReader(BaseXLS):
    """ Class use to read XLS file.
    To make this class compatible with all models and with constraints template, this last have been
    build for return data without business rules. Data process will be done in Import View.

    Attributes
    ----------
    file : file
        XLS File to import.
    workbook : openpyxl.Workbook
        Workbook of file.
    """

    def __init__(self, file):
        super().__init__()

        self.file = file
        self.workbook = load_workbook(file)

    def read_row(self, sheet_name):
        """ Read current line of file.

        Parameters
        ----------
        sheet_name : string
            Name of sheet readed.

        Returns
        -------
        dict
            Data of the current line group by column. We return only valid data.
            If a column is empty, this column isn't added in return value.
            {
                'A': 'data_in_column_A',
                'C': 'data_in_column_C'
            }

        Notes
        -----
        This method automatically jumps to the next row.
        """
        sheet = self.workbook[sheet_name]
        line = self.nrows[sheet_name]
        row = Row(line=line)
        try:
            for cell in sheet.rows[line - 1]:
                if cell.value is not None and cell.value != ' ':
                    row[cell.column] = cell.value
            self.jump_to_next_row(sheet_name)
        except IndexError:  # If no rows is available -> end of file.
            return {}
        return row

    def process_table_row(self, headers, row):
        """ Process a row of table.
        We use headers and row for create a dictionary which contains headers with their
        associated value.

        Parameters
        ----------
        headers : dict
            Contains headers of the table.
        row : dict
            the line to be processed

        Returns
        -------
        dict
            Contains headers with their associated value.
            {
                header1: value1,
                header2: value2,
            }
        """
        row_data = Row(line=row.line)
        for column, value in row.items():
            header = headers[column].lower()
            row_data[header] = value
        return row_data

    def read_table(self, sheet_name):
        """ Read table which contains objects relying on object to import.
        Use this method to capture objects list which are dependent on the objects you want to
        create.
        For example, if we create a Brainstormer, we could have a table with BrainstormerProposition
        and/or BrainstormerPropositionGroup.

        Parameters
        ----------
        sheet_name : string
            Name of sheet readed.

        Returns
        -------
        list
            Contains headers with their associated value for each row.
            [{
                header1: value1,
                header2: value2,
            }, ...]
        """
        data = []
        row_headers = self.read_row(sheet_name)

        row = self.read_row(sheet_name)
        if not row:  # If first line is empty, we jump, it's a table empty
            self.jump_to_next_row(sheet_name)

        while row:
            row_data = self.process_table_row(row_headers, row)
            data.append(row_data)
            row = self.read_row(sheet_name)
        return data

    def read_values(self, sheet_name, number_of_lines_to_read):
        """ Read many values.

        Parameters
        ----------
        sheet_name : string
            Name of sheet read.
        number_of_lines_to_read : int
            Number of values to read

        Returns
        -------
        list(string)
            List of detail value
        """
        begin_line = str(self.STYLE.TOP_OFFSET)
        last_line = str(self.STYLE.TOP_OFFSET + number_of_lines_to_read)
        line = begin_line + '-' + last_line
        values = Row(line=line)
        for __ in range(number_of_lines_to_read):
            table = self.read_table(sheet_name)
            try:
                data_object = table[0]
                values.update(data_object)
            except IndexError:
                continue
        return values

    def read_value(self, sheet_name):
        """ Read one value.
        Use this method to capture value of a field like id, title or other field which describe
        the object we want to create.


        Parameters
        ----------
        sheet_name : string
            Name of sheet readed.

        Returns
        -------
        string
            Value of detail
        """
        return self.read_values(sheet_name, 1)

    def initialize(self):
        """ Initialize variables """
        for sheet_name in self.workbook.get_sheet_names():
            self.nrows[sheet_name] = self.STYLE.TOP_OFFSET


class XLSWriter(BaseXLS):
    """ Class use to write XLS file.

    Attributes
    ----------
    file : file
        XLS File to import.
    workbook : openpyxl.Workbook
        Workbook of file.
    titles : dict
        Collection of sheet titles.
    widths : dict
        Collection of width cells.
    header_info : string
        Information to add in header.
    header_title : string
        Title to add in header.
    """

    def __init__(self, file, header_info='Magency Training Plateform', header_title='Export'):
        super().__init__()

        self.file = file
        self.workbook = Workbook()
        self.titles = {}
        self.widths = {}
        self.header_info = header_info
        self.header_title = header_title
        self.cell_title = self.STYLE.CELL_TITLE

    def create_new_sheet(self, sheet_name):
        """ Create a new sheet in workbook. """
        sheet_name = sheet_name[:30]
        return self.workbook.create_sheet(title=sheet_name)

    def get_field(self, obj, field):
        """ Get the value of object field.

        This method accept relation like `group__name` in name field.
        Or `dimensions__code 2` because dimensions is a M2M.

        Parameters
        ----------
        obj : object
            Object whose we want retrieve the value of field
        field : string
            Name of field

        Returns
        -------
        string
            Value of object field.
        """
        index = None
        if ' ' in field:  # M2M case where we choose an element
            field, index = field.split(' ', 1)
            index = int(index)
        attrs = field.split('__')
        value = obj
        for attr in attrs:
            try:
                value = value.all()
                value = getattr(value[index - 1], attr)
                index = None  # Once the index is used, it is "deactivated"
            except (IndexError, AttributeError):
                try:
                    value = getattr(value, attr)
                except AttributeError:
                    value = None
                    break
        return value

    def update_row_width(self, column, sheet_name, new_elmt):
        """ Update width of column according to size of the new value.

        Parameters
        ----------
        column : int
            Number of column
        sheet_name : string
            Name of sheet
        new_elmt : object
            New element added
        """
        new_elmt = str(new_elmt)
        width = len(new_elmt)
        if width > self.STYLE.COLUMN_MAX_WIDTH:
            width = self.STYLE.COLUMN_MAX_WIDTH
        width = int(width * self.STYLE.ESCAPE_RATIO) + self.STYLE.WIDTH_MARGIN  # Compute width
        if width > self.widths[sheet_name].get(column, 0):
            self.widths[sheet_name][column] = width
            self.workbook[sheet_name].column_dimensions[get_column_letter(column)].width = width

    def write_row(self, row, sheet_name, style_name='DEFAULT_NO_PROTECTED'):
        """ Write row provided in sheet.

        Parameters
        ----------
        row : list
            List of value to write
        sheet_name : string
            Name of sheet
        style_name : string
            Cells style by default
        """
        sheet = self.workbook[sheet_name]
        nrow = self.nrows[sheet_name]
        for index, elmt in enumerate(row):
            cell = sheet.cell(row=nrow, column=index + StyleXlsx.LEFT_OFFSET + 1)
            if isinstance(elmt, dict):
                value = elmt.get('value')
                style_name = elmt.get('style')
                max_length = elmt.get('max_length', None)
                if max_length:
                    data_validation = DataValidation(
                        type="textLength",
                        operator="lessThanOrEqual",
                        formula1=str(max_length)
                    )
                    sheet.add_data_validation(data_validation)
                    data_validation.add(cell)
                try:
                    comment = elmt['comment']
                    cell.comment = comment
                except KeyError:
                    pass
                try:
                    conditions = elmt['conditions']
                    for condition in conditions:
                        sheet.conditional_formatting.add(cell.coordinate, condition)
                except KeyError:
                    pass
            else:
                value = elmt
            if value is None:
                value = ' '
            cell.value = str(value)
            if style_name not in self.STYLE.STYLES:
                style_name = 'DEFAULT_NO_PROTECTED'
            cell.style = self.STYLE.STYLES[style_name]
            self.update_row_width(index + self.STYLE.LEFT_OFFSET + 1, sheet_name, value)
        self.jump_to_next_row(sheet_name)

    def write_rows(self, rows, sheet_name, style='DEFAULT_NO_PROTECTED'):
        """ Write rows provided in sheet.

        Note
        -----
        This method use `writerow` method.

        Parameters
        ----------
        rows : list(list)
            List of list value to write
        sheet_name : string
            Name of sheet
        style_name : string
            Cells style by default
        """
        for row in rows:
            self.write_row(row, sheet_name, style)

    def create_headers(self, fields, comments=None):
        """ Create headers with name fields and comments.

        Parameters
        ----------
        fields : list
            List of fields
        comments : dict
            Comments to add by header

        Returns
        -------
        list
            List contains headers with style corresponding
        """
        row = []
        for field in fields:
            try:
                comment_text = comments[field]
                comment = Comment(comment_text, '')
                row.append({'value': field.upper(), 'style': 'HEADER_TABLE', 'comment': comment})
            except (TypeError, KeyError):
                row.append({'value': field.upper(), 'style': 'HEADER_TABLE'})
        return row

    def get_data(self, obj, resource, display_fields):
        """ Get data in object to export.

        Parameters
        ----------
        obj : object
            Object to export
        fields : list
            List of fields

        Returns
        -------
        list
            List contains headers with style corresponding
        """
        row = list()
        for display_field in display_fields:
            field = resource.display_field_to_field_name(display_field)
            if field in resource.get_fields(allow_nested=False):
                try:
                    value = getattr(resource, 'get_' + field)(obj)
                except AttributeError:
                    value = self.get_field(obj, field)
                row.append(value)
            else:
                row.append(None)
        return row

    def write_table(self, sheet_name, queryset, resource):
        """ Write a table of data.

        Example
        -------
        - data1
        - data2
        - data3

        Parameters
        ----------
        sheet_name : string
            Name of sheet
        queryset : list
            List of objects to write
        resource : resource
            Resource used for export objects in queryset
        """
        rows = list()
        display_fields = resource.get_display_fields()
        comments = resource.header_comments
        rows.append(self.create_headers(display_fields, comments))

        for obj in queryset:
            rows.append(self.get_data(obj, resource, display_fields))

        self.write_rows(rows, sheet_name)
        self.jump_to_next_row(sheet_name)

    def write_nested_table(self, sheet_name, obj, resource):
        """Write a table of nested data.

        Example
        -------
        - data1
        -- data11
        - data2
        -- data21
        -- data22

        Parameters
        ----------
        sheet_name : string
            Name of sheet
        obj : object
            Main object used to extract nested objects
        nested_objects : tuple
            Collection of Link and Resource
        """
        rows = list()
        display_fields = resource.get_display_fields(only_nested=True)
        # comments = resource.get_header_comments()
        rows.append(self.create_headers(display_fields))

        def generate_nested_objects(current_obj, obj_resource):
            for nested_link, nested_resource in obj_resource.get_nested_resource(current_obj):
                if isinstance(nested_link, str):
                    attr = getattr(current_obj, nested_link)
                    try:
                        objects = attr()
                    except KeyError:
                        objects = attr.filter(is_deleted=False)
                    except TypeError:
                        objects = [attr]
                elif isinstance(nested_link, QuerySet):
                    objects = nested_link.filter(is_deleted=False)

                for item in objects:
                    rows.append(self.get_data(item, nested_resource, display_fields))
                    if nested_resource.nested_resource:
                        generate_nested_objects(item, nested_resource)
                    else:
                        continue

        generate_nested_objects(obj, resource)

        self.write_rows(rows, sheet_name)
        self.jump_to_next_row(sheet_name)

    def write_value(self, sheet_name, obj, field_name, field_display):
        """ Write value of a object field.

        Parameters
        ----------
        sheet_name : string
            Name of sheet
        obj : object
            Object to export
        field : string
            Name of field
        """
        rows = list()
        rows.append([{'value': field_display.upper(), 'style': 'HEADER_TABLE'}])
        rows.append([{'value': self.get_field(obj, field_name)}])
        self.write_rows(rows, sheet_name)
        self.jump_to_next_row(sheet_name)

    def write_values(self, sheet_name, obj, resource):
        """ Write values of an object according this resource.

        Parameters
        ----------
        sheet_name : string
            Name of sheet
        obj : object
            Object to export
        resource : mtp.apps.import_export.resources.Resource
            Resource used to extract object
        """
        display_fields = resource.get_display_fields(allow_nested=False)
        for index, field in enumerate(resource.get_fields(allow_nested=False)):
            self.write_value(sheet_name, obj, field, display_fields[index])

    def initialize(self, sheet_name, new_sheet_name=None):
        """ Initialize variables and title of sheet if provided.

        Parameters
        ----------
        name : string
            Name of the sheet.
        title : string
            Title of sheet.

        Returns
        -------
        string
            Return the new sheet_name

        """
        if new_sheet_name:
            self.workbook[sheet_name].title = new_sheet_name[:30]  # We update title of sheet
            sheet_name = new_sheet_name[:30]

        # Update internal variables
        self.titles[sheet_name] = sheet_name.title()
        self.widths[sheet_name] = {}
        self.nrows[sheet_name] = self.STYLE.TOP_OFFSET

        return sheet_name

    def remove_all_sheet(self):
        """ Remove all sheet in worbook. """
        for sheet in self.workbook:
            self.workbook.remove_sheet(sheet)

    def save(self):
        """ Save Workbook.
        Add title and set dimensions.
        """
        for sheet_name in self.workbook.get_sheet_names():
            sheet = self.workbook[sheet_name]

            # Add sheet tile
            cell = sheet[self.cell_title]
            cell.value = self.titles[sheet.title]

            # Style cells
            self.STYLE.apply_style(sheet)
            self.STYLE.dimension_sheet(sheet)
            self.STYLE.protect_sheet(sheet)

        self.workbook.save(self.file)
