import os
import sys
import shutil

from jinja2 import Environment

from ..utils import LicenseHandler
from ...main import Book
from ...constants import FixedFormatType, FixedFormatQuality

try:
    import PIL
except ImportError:
    PIL = None

try:
    from matplotlib.figure import Figure
except ImportError:
    Figure = None

try:
    import numpy as np
except ImportError:
    np = None

try:
    import pandas as pd
except ImportError:
    pd = None

LicenseHandler.validate_license('reports')
string_types = (str, )  # in case we need to reintroduce py27 compatibility


def create_report(template, output, book_settings=None, app=None, **data):
    """
    This feature requires xlwings :guilabel:`PRO`.

    Writes the values of all key word arguments to the ``output`` file according to the ``template`` and the variables
    contained in there (Jinja variable syntax).
    Following variable types are supported:

    strings, numbers, lists, simple dicts, NumPy arrays, Pandas DataFrames, PIL Image objects that have a filename and
    Matplotlib figures.

    Parameters
    ----------
    template: str
        Path to your Excel template, e.g. ``r'C:\\Path\\to\\my_template.xlsx'``

    output: str
        Path to your Report, e.g. ``r'C:\\Path\\to\\my_report.xlsx'``

    book_settings: dict, default None
        A dictionary of ``xlwings.Book`` parameters, for details see: :attr:`xlwings.Book`.
        For example: ``book_settings={'update_links': False}``.

    app: xlwings App, default None
        By passing in an xlwings App instance, you can control where your report runs and configure things like ``visible=False``.
        For details see :attr:`xlwings.App`. By default, it creates the
        report in the currently active instance of Excel.

    data: kwargs
        All key/value pairs that are used in the template.

    Returns
    -------
    wb: xlwings Book


    Examples
    --------
    In ``my_template.xlsx``, put the following Jinja variables in two cells: ``{{ title }}`` and ``{{ df }}``

    >>> from xlwings.pro.reports import create_report
    >>> import pandas as pd
    >>> df = pd.DataFrame(data=[[1,2],[3,4]])
    >>> wb = create_report('my_template.xlsx', 'my_report.xlsx', title='MyTitle', df=df)

    With many template variables it may be useful to collect the data first:

    >>> data = dict(title='MyTitle', df=df)
    >>> wb = create_report('my_template.xlsx', 'my_report.xlsx', **data)

    If you need to handle external links or a password, use it like so:

    >>> wb = create_report('my_template.xlsx', 'my_report.xlsx',
                           book_settings={'update_links': True, 'password': 'mypassword'},
                           **data)

    You can control the Excel instance by passing in an xlwings App instance. For example, to
    run the report in a separate and hidden instance of Excel, do the following:

    >>> import xlwings as xw
    >>> from xlwings.pro.reports import create_report
    >>> app = xw.App(visible=False)  # Separate and hidden Excel instance
    >>> wb = create_report('my_template.xlsx', 'my_report.xlsx', app=app, **data)
    >>> app.quit()  # Close the wb and quit the Excel instance
    """
    shutil.copyfile(template, output)
    if app:
        if book_settings:
            wb = app.books.open(output, **book_settings)
        else:
            wb = app.books.open(output)
    else:
        # Use existing Excel instance or create a new one if there is none
        if book_settings:
            wb = Book(output, **book_settings)
        else:
            wb = Book(output)

    # On Windows, Excel will not move objects correctly with screen_updating = False during row insert/delete operations
    # So we'll need to set it to True before any such operations. Getting origin state here to revert to.
    screen_updating_original_state = wb.app.screen_updating

    env = Environment()
    locals().update(data)

    for sheet in wb.sheets:
        # used_range doesn't start automatically in A1
        last_cell = sheet.used_range.last_cell
        values_all = sheet.range((1, 1), (last_cell.row, last_cell.column)).options(ndim=2).value if sheet.used_range.value else []
        # Frame markers
        frame_markers = []
        if values_all and '<frame>' in values_all[0]:
            frame_markers = values_all[0]
            values = values_all[1:]
            if sys.platform.startswith('win'):
                wb.app.screen_updating = True
            sheet['1:1'].delete('up')
            wb.app.screen_updating = screen_updating_original_state
            frame_indices = [i for i, val in enumerate(frame_markers) if val == '<frame>']
            frame_indices += [0, last_cell.column]
            frame_indices = list(sorted(set(frame_indices)))
        else:
            values = values_all
            frame_indices = [0, last_cell.column]
        values_per_frame = []
        for ix in range(len(frame_indices) - 1):
            values_per_frame.append([i[frame_indices[ix]:frame_indices[ix + 1]] for i in values])
        # Loop through every cell for each frame
        for ix, values in enumerate(values_per_frame):
            row_shift = 0
            for i, row in enumerate(values):
                for j, value in enumerate(row):
                    if isinstance(value, string_types):
                        tokens = list(env.lex(value))
                        if value.count('{{') == 1 and tokens[0][1] == 'variable_begin' and tokens[-1][1] == 'variable_end':
                            # Cell contains single Jinja variable
                            var = None
                            for _, token_type, value in tokens:
                                if token_type == 'variable_begin':
                                    var = ''
                                elif token_type == 'variable_end':
                                    result = eval(var)
                                    if PIL and isinstance(result, PIL.Image.Image):
                                        # TODO: properly support Image objects in xlwings
                                        sheet.pictures.add(result.filename,
                                                           top=sheet[i + row_shift, j + frame_indices[ix]].top,
                                                           left=sheet[i + row_shift, j + frame_indices[ix]].left,
                                                           width=result.width, height=result.height)
                                        sheet[i + row_shift, j + frame_indices[ix]].value = None
                                    elif Figure and isinstance(result, Figure):
                                        # Matplotlib figures
                                        sheet.pictures.add(result,
                                                           top=sheet[i + row_shift, j + frame_indices[ix]].top,
                                                           left=sheet[i + row_shift, j + frame_indices[ix]].left)
                                        sheet[i + row_shift, j + frame_indices[ix]].value = None
                                    else:
                                        # Simple Jinja variables
                                        # Check for height of 2d array
                                        if isinstance(result, (list, tuple)) and isinstance(result[0], (list, tuple)):
                                            result_len = len(result)
                                        elif np and isinstance(result, np.ndarray):
                                            result_len = len(result)
                                        elif pd and isinstance(result, pd.DataFrame):
                                            # TODO: handle MultiIndex headers
                                            result_len = len(result) + 1
                                        else:
                                            result_len = 1
                                        # Insert rows if within <frame> and 'result' is multiple rows high
                                        rows_to_be_inserted = 0
                                        if frame_markers and result_len > 1:
                                            # Deduct header and first data row that are part of template
                                            rows_to_be_inserted = result_len - 2
                                            if rows_to_be_inserted > 0:
                                                if sys.platform.startswith('win'):
                                                    wb.app.screen_updating = True
                                                # Since CopyOrigin is not supported on Mac, we start copying two rows
                                                # below the header so the data row formatting gets carried over
                                                end_column = frame_indices[ix] + len(values[0])
                                                sheet.range((i + row_shift + 3, j + frame_indices[ix] + 1),
                                                            (i + row_shift + rows_to_be_inserted + 2, end_column)).insert('down')
                                                # Inserting does not take over borders
                                                sheet.range((i + row_shift + 2, j + frame_indices[ix] + 1),
                                                            (i + row_shift + 2, end_column)).copy()
                                                sheet.range((i + row_shift + 2, j + frame_indices[ix] + 1),
                                                            (i + row_shift + rows_to_be_inserted + 2, end_column)).paste(paste='formats')
                                                wb.app.screen_updating = screen_updating_original_state
                                        sheet[i + row_shift, j + frame_indices[ix]].value = result
                                        row_shift += rows_to_be_inserted
                                elif var is not None and token_type not in ('whitespace',):
                                    var += value
                        elif '{{' in value:
                            # These are strings with (multiple) Jinja variables so apply standard text rendering here
                            template = env.from_string(value)
                            sheet[i + row_shift, j + frame_indices[ix]].value = template.render(data)
                        else:
                            # Don't do anything with cells that don't contain any templating so we don't lose the formatting
                            pass
    wb.save()
    return wb


def create_pdf(book, output=None, from_=None, to=None):
    """
    Creates a PDF report from the Excel workbook. NOTE: This is currently only availalbe on Windows.

    Parameters
    ----------
    book: xlwings Book
        An xlwings Book, e.g. ``xw.Book(...)`` or the object returned by ``create_report``.

    output: str, optional
        Path to the PDF report, e.g. ``C:\\my_report.pdf``. By default, the report will be called the same as
        the Excel file, but with the ``.pdf`` extension.

    ``from_`` : int, optional
        First sheet that will be included in the PDF report (0 based indexing).

    to: int, optional
        Last sheet that will be included in the PDF report (0 based indexing).

    Returns
    -------
    output: str
        pdf file name

    Examples
    --------
    >>> from xlwings.pro.reports import create_report, create_pdf
    >>> import pandas as pd
    >>> df = pd.DataFrame(data=[[1,2],[3,4]])
    >>> wb = create_report('my_template.xlsx', 'my_report.xlsx', title='MyTitle', df=df)
    >>> create_pdf(wb, from_=0, to=0)
    """
    if sys.platform.startswith('win'):
        if output is None:
            filename, extension = os.path.splitext(book.fullname)
            output = filename + '.pdf'
        from_to = {}
        if from_ is not None:
            from_to.update({'From': from_ + 1})
        if to is not None:
            from_to.update({'To': to + 1})

        book.api.ExportAsFixedFormat(Type=FixedFormatType.xlTypePDF,
                                     Filename=output,
                                     Quality=FixedFormatQuality.xlQualityStandard,
                                     IncludeDocProperties=True,
                                     IgnorePrintAreas=False,
                                     OpenAfterPublish=False,
                                     **from_to)
        book.save()
        return output
    else:
        raise OSError('create_pdf is currently not supported on macOS.')

