# AUTOGENERATED! DO NOT EDIT! File to edit: xlsx.ipynb (unless otherwise specified).

__all__ = "to_xlsx ColumnFormat format_sheet append_sheet".split()

import openpyxl
from openpyxl.worksheet.table import Table, TableStyleInfo
import re
from pathlib import Path
import toolz.curried as tz
from fnmatch import fnmatch
from functools import partial
from typing import NewType, Dict, List, Iterable, Any, Optional, Union, Tuple
from typing import NamedTuple


class CellFormat(NamedTuple):
    number_format: str
    alignment: openpyxl.styles.Alignment
    width: int
    font: openpyxl.styles.Font


class ColumnFormat(NamedTuple):
    match: str
    number_format: str=""
    width: int=15
    valign: str="top"
    halign: str="left"
    header_valign: str="bottom"
    header_halign: str="left"
    wrap_text: bool=True
    num_align: bool=True
    kwargs: Optional[Dict[str,Any]]=None

    @property
    def cell(self):
        return CellFormat(
            self.number_format,
            openpyxl.styles.Alignment(
                vertical=self.valign,
                horizontal="right" if self.num_align and self.number_format else self.halign,
                wrap_text=self.wrap_text,
            ),
            self.width,
            openpyxl.styles.Font(bold=False)
        )

    @property
    def header(self):
        return CellFormat(
            self.number_format,
            openpyxl.styles.Alignment(
                vertical=self.header_valign,
                horizontal="right" if self.num_align and self.number_format else self.header_halign,
                wrap_text=self.wrap_text,
            ),
            self.width,
            openpyxl.styles.Font(bold=True)
        )


def get_cell_attrs(
    file: Path,
    sheet_name: str,
    attr: str,
    rows_to_review: int=100
    ):
    """Returns the dict of columns where each value is also a dict containing
    the value counts of the attr values."""
    wb = openpyxl.load_workbook(str(file), read_only=True)
    ws = wb[sheet_name]
    rows = ws.rows
    header = next(rows)
    # 1st row contains column headers
    names = [cell.value for cell in header]
    letters = [cell.column_letter for cell in header]
    return tz.pipe(
        # Loops over rows in sheet
        rows,
        tz.take(rows_to_review),
        # For each row, create a dict usng names has keys
        tz.map(lambda row: dict(zip(names, row))),
        # Get the .xlsx data_type for each cell
        tz.map(tz.valmap(tz.flip(getattr, attr))),
        # Combine cells into a list per column
        tz.merge_with(list),
        # Count the cells for each data type in the column
        tz.valmap(tz.frequencies),
    )

class Formats(NamedTuple):
    """Aggregrates column formats for a given file and sheet_name."""
    columns: Tuple[ColumnFormat]

    @classmethod
    def from_sheet(cls, file: Path, sheet_name: str, rows_to_review: int=100):
        """Analyzes and existing sheet and returns a Patterns object which
        provides default text alignment, column width, and number_format
        options based upon how the .xlsx file has stored the data.
        """
        data_types = get_cell_attrs(file, sheet_name, "data_type",
            rows_to_review)

        return cls(tuple(
            # If any cells have a date freq
            ColumnFormat(column, "yyyy-mm-dd") if "d" in freq else
            # If any cells have a string freq
            ColumnFormat(column) if "s" in freq else
            # Default data type in .xslx is "n" for number
            ColumnFormat(column, "#,##0.00")
            for column, freq in data_types.items()
        ))


    @classmethod
    def from_user(
        cls,
        file: Path,
        sheet_name: str,
        *formats: ColumnFormat,
        rows_to_review: int=100):
        """Uses from_sheet to get default formats and then applies any formats
        provided by the user."""

        defaults = cls.from_sheet(file, sheet_name)
        if len(formats) == 0:
            return defaults

        results = {}
        for default in defaults.columns:
            column = default.match
            seen = column in results
            for format in formats:
                assert isinstance(format, ColumnFormat), \
                    "Provided format is not ColumnFormat"
                if fnmatch(format.match, column) and not seen:
                    results[column] = format

            if column not in results:
                results[column] = default

        return cls(tuple(
            v._replace(match=k)
            for k, v in results.items()
        ))

    def to_dict(self) -> Dict[str,ColumnFormat]:
        return {
            column.match: column
            for column in self.columns
        }


def format_sheet(
    file: Path,
    sheet_name: str,
    *formats: ColumnFormat,
    table_name: str="",
    freeze_panes: "cell_refrence"="A2"
    ) -> Path:
    """Formats an existing sheet and optionally adds the sheet as a
    Worksheet table with a default table format.

    Provided formats for the 'patterns' argument use a Unix shell-style
    wildcard as the key and the value is an XLSXFormat. The XLSXFormat is
    typically created by using the column_format function.

    Unix shell-style pattern
    ╒═══════════╤══════════════════════════════════╕
    │ Pattern   │ Meaning                          │
    ╞═══════════╪══════════════════════════════════╡
    │ *         │ matches everything               │
    ├───────────┼──────────────────────────────────┤
    │ ?         │ matches any single character     │
    ├───────────┼──────────────────────────────────┤
    │ [seq]     │ matches any character in seq     │
    ├───────────┼──────────────────────────────────┤
    │ [!seq]    │ matches any character not in seq │
    ╘═══════════╧══════════════════════════════════╛
    """
    # Load file
    wb = openpyxl.load_workbook(file)
    ws = wb[sheet_name]

    # Get column names and calc formats
    rows = ws.rows
    column_names = [cell.value for cell in next(rows)]
    formats = Formats.from_user(file, sheet_name, *formats).to_dict()

    # Loop by column and format column
    for col in ws.iter_cols():
        header = col[0]
        cells = col[1:]
        name = header.value
        letter = header.column_letter

        fmt = formats[name]

        # Set width first so that text wrap works
        ws.column_dimensions[letter].width = fmt.width

        # Format header cell
        header.alignment = fmt.header.alignment
        header.number_format = fmt.header.number_format
        header.font = fmt.header.font

        # Add alignment and number format
        for cell in cells:
            cell.alignment = fmt.cell.alignment
            cell.number_format = fmt.cell.number_format
            cell.font = fmt.cell.font

    # Freeze panes
    ws.freeze_panes = ws[freeze_panes]

    # Add table
    table_name = table_name or sheet_name
    table_name = table_name.replace(" ", "_")
    table = Table(displayName=table_name, ref=ws.dimensions)
    style = TableStyleInfo(name="TableStyleLight1", showFirstColumn=False, showColumnStripes=False, showRowStripes=True)
    table.tableStyleInfo = style
    ws.add_table(table)

    # Save file
    wb.save(str(file))
    wb.close()

    return file


def to_xlsx(
    data: Iterable[Dict],
    file: Path,
    sheet_name: str,
    *formats: ColumnFormat,
    apply_default_formats: bool=True,
    over_write: bool=True
    ) -> Path:
    """Allows data to be appended to a .xlsx file as a new sheet.

    If formats are provided, format_sheet will be called to format the sheet
    and optionally adds the sheet as a Worksheet table with a default
    table format.

    Provided formats for the 'patterns' argument use a Unix shell-style
    wildcard as the key and the value is an XLSXFormat. The XLSXFormat is
    typically created by using the column_format function.

    Unix shell-style pattern
    ╒═══════════╤══════════════════════════════════╕
    │ Pattern   │ Meaning                          │
    ╞═══════════╪══════════════════════════════════╡
    │ *         │ matches everything               │
    ├───────────┼──────────────────────────────────┤
    │ ?         │ matches any single character     │
    ├───────────┼──────────────────────────────────┤
    │ [seq]     │ matches any character in seq     │
    ├───────────┼──────────────────────────────────┤
    │ [!seq]    │ matches any character not in seq │
    ╘═══════════╧══════════════════════════════════╛
    """
    if not over_write and file.exists():
        wb = openpyxl.load_workbook(file)
        ws = wb.create_sheet(sheet_name)
    else:
        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = sheet_name
    for row_num, row in enumerate(data):
        if row_num == 0:
            ws.append([k for k in row])
        ws.append(v for v in row.values())
    wb.save(str(file))
    if len(formats) > 0:
        format_sheet(file, sheet_name, *formats)
    elif apply_default_formats:
        format_sheet(file, sheet_name)

    wb.close()
    return file


append_sheet = partial(to_xlsx, over_write=False)
append_sheet.__doc__ = "Same as to_xlsx except over_write=False"