"""
pyexcel.internal.meta
~~~~~~~~~~~~~~~~~~~~~~

Annotate sheet and book class' attributes

:copyright: (c) 2015-2025 by Onni Software Ltd.
:license: New BSD License
"""

from functools import partial

from pyexcel import constants
from pyexcel import docstrings as docs
from pyexcel._compact import append_doc
from pyexcel.internal import SOURCE
from pyexcel.internal.core import save_book, save_sheet, get_sheet_stream
from pyexcel.internal.utils import make_a_property


def make_presenter(source_getter, attribute=None):
    """make a custom presentation method for each file types"""

    def custom_presenter(self, **keywords):
        """docstring is assigned a few lines down the line"""
        keyword = SOURCE.get_keyword_for_parameter(attribute)
        keywords[keyword] = attribute
        memory_source = source_getter(**keywords)
        memory_source.write_data(self)
        try:
            content_stream = memory_source.get_content()
            content = content_stream.getvalue()
        except AttributeError:
            # python 3 _io.TextWrapper
            content = None

        return content

    custom_presenter.__doc__ = f"Get data in {attribute} format"
    return custom_presenter


def sheet_presenter(attribute=None):
    """make a custom presentation method for sheet"""
    source_getter = SOURCE.get_writable_source
    return make_presenter(source_getter, attribute)


def book_presenter(attribute=None):
    """make a custom presentation method for book"""
    source_getter = SOURCE.get_writable_book_source
    return make_presenter(source_getter, attribute)


def importer(attribute=None):
    """make a custom input method for sheet"""

    def custom_importer1(self, content, **keywords):
        """docstring is assigned a few lines down the line"""
        sheet_params = {}
        for field in constants.VALID_SHEET_PARAMETERS:
            if field in keywords:
                sheet_params[field] = keywords.pop(field)
        keyword = SOURCE.get_keyword_for_parameter(attribute)
        if keyword == "file_type":
            keywords[keyword] = attribute
            keywords["file_content"] = content
        else:
            keywords[keyword] = content
        named_content = get_sheet_stream(**keywords)
        self.init(named_content.payload, named_content.name, **sheet_params)

    custom_importer1.__doc__ = f"Set data in {attribute} format"
    return custom_importer1


def book_importer(attribute=None):
    """make a custom input method for book"""

    def custom_book_importer(self, content, **keywords):
        """docstring is assigned a few lines down the line"""
        keyword = SOURCE.get_keyword_for_parameter(attribute)
        if keyword == "file_type":
            keywords[keyword] = attribute
            keywords["file_content"] = content
        else:
            keywords[keyword] = content
        sheets, filename, path = _get_book(**keywords)
        self.init(sheets=sheets, filename=filename, path=path)

    custom_book_importer.__doc__ = f"Set data in {attribute} format"
    return custom_book_importer


def attribute(
    cls,
    file_type,
    instance_name="Sheet",
    description=constants.OUT_FILE_TYPE_DOC_STRING,
    **keywords,
):
    """
    create custom attributes for each class
    """
    doc_string = description.format(file_type, instance_name)
    make_a_property(cls, file_type, doc_string, **keywords)


REGISTER_PRESENTATION = partial(
    attribute,
    getter_func=sheet_presenter,
    description=constants.OUT_FILE_TYPE_DOC_STRING,
)
REGISTER_BOOK_PRESENTATION = partial(
    attribute,
    getter_func=book_presenter,
    instance_name="Book",
    description=constants.OUT_FILE_TYPE_DOC_STRING,
)
REGISTER_INPUT = partial(
    attribute,
    setter_func=importer,
    description=constants.IN_FILE_TYPE_DOC_STRING,
)
REGISTER_BOOK_INPUT = partial(
    attribute,
    instance_name="Book",
    setter_func=book_importer,
    description=constants.IN_FILE_TYPE_DOC_STRING,
)
REGISTER_IO = partial(
    attribute,
    getter_func=sheet_presenter,
    setter_func=importer,
    description=constants.IO_FILE_TYPE_DOC_STRING,
)
REGISTER_BOOK_IO = partial(
    attribute,
    getter_func=book_presenter,
    setter_func=book_importer,
    instance_name="Book",
    description=constants.IO_FILE_TYPE_DOC_STRING,
)


class StreamAttribute:
    """Provide access to get_*_stream methods"""

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

    def __getattr__(self, name):
        getter = getattr(self.cls, "save_to_memory")
        return getter(file_type=name)


class PyexcelObject:
    """parent class for pyexcel.Sheet and pyexcel.Book"""

    @property
    def stream(self):
        """Return a stream in which the content is properly encoded

        Example::

            >>> import pyexcel as p
            >>> b = p.get_book(bookdict={"A": [[1]]})
            >>> csv_stream = b.stream.texttable
            >>> print(csv_stream.getvalue())
            A:
            +---+
            | 1 |
            +---+

        Where b.stream.xls.getvalue() is equivalent to b.xls. In some situation
        b.stream.xls is preferred than b.xls.

        Sheet examples::

            >>> import pyexcel as p
            >>> s = p.Sheet([[1]], 'A')
            >>> csv_stream = s.stream.texttable
            >>> print(csv_stream.getvalue())
            A:
            +---+
            | 1 |
            +---+

        Where s.stream.xls.getvalue() is equivalent to s.xls. In some situation
        s.stream.xls is preferred than s.xls.

        It is similar to :meth:`~pyexcel.Book.save_to_memory`.
        """
        return StreamAttribute(self)

    def save_to_memory(self, file_type, **keywords):
        """Save the content to memory

        :param file_type: any value of 'csv', 'tsv', 'csvz',
                          'tsvz', 'xls', 'xlsm', 'xlsm', 'ods'
        :param stream: the memory stream to be written to. Note in
                       Python 3, for csv  and tsv format, please
                       pass an instance of StringIO. For xls, xlsx,
                       and ods, an instance of BytesIO.
        """
        raise NotImplementedError("save to memory is not implemented")

    def plot(self, file_type="svg", **keywords):
        """
        Visualize the data

        Parameters:
        -----------------

        file_type:string
           'svg' by default. 'png', 'jpeg' possible depending on plugins

        chart_type:string
           'bar' by default. other chart types are subjected to plugins.
        """
        memory_content = self.save_to_memory(file_type, **keywords)
        if file_type in ["png", "svg", "jpeg"]:
            # make the signature for jypter notebook
            def get_content(self):
                return self.getvalue().decode("utf-8")

            setattr(
                memory_content,
                f"_repr_{file_type}_",
                partial(get_content, memory_content),
            )
        return memory_content

    def _repr_html_(self):
        return self.html

    def __repr__(self):
        return self.texttable

    def __str__(self):
        return self.__repr__()


class SheetMeta(PyexcelObject):
    """Annotate sheet attributes"""

    register_io = classmethod(REGISTER_IO)
    register_presentation = classmethod(REGISTER_PRESENTATION)
    register_input = classmethod(REGISTER_INPUT)

    @append_doc(docs.SAVE_AS_OPTIONS)
    def save_as(self, filename, **keywords):
        """Save the content to a named file"""
        return save_sheet(self, file_name=filename, **keywords)

    def save_to_memory(self, file_type, stream=None, **keywords):
        stream = save_sheet(
            self,
            file_type=file_type,
            file_stream=stream,
            **keywords,
        )
        return stream

    def save_to_django_model(
        self,
        model,
        initializer=None,
        mapdict=None,
        batch_size=None,
    ):
        """Save to database table through django model

        :param model: a database model
        :param initializer: a initialization functions for your model
        :param mapdict: custom map dictionary for your data columns
        :param batch_size: a parameter to Django concerning the size
                           for bulk insertion
        """
        save_sheet(
            self,
            model=model,
            initializer=initializer,
            mapdict=mapdict,
            batch_size=batch_size,
        )

    def save_to_database(
        self,
        session,
        table,
        initializer=None,
        mapdict=None,
        auto_commit=True,
    ):
        """Save data in sheet to database table

        :param session: database session
        :param table: a database table
        :param initializer: a initialization functions for your table
        :param mapdict: custom map dictionary for your data columns
        :param auto_commit: by default, data is auto committed.

        """
        save_sheet(
            self,
            session=session,
            table=table,
            initializer=initializer,
            mapdict=mapdict,
            auto_commit=auto_commit,
        )


class BookMeta(PyexcelObject):
    """Annotate book attributes"""

    register_io = classmethod(REGISTER_BOOK_IO)
    register_presentation = classmethod(REGISTER_BOOK_PRESENTATION)
    register_input = classmethod(REGISTER_BOOK_INPUT)

    @append_doc(docs.SAVE_AS_OPTIONS)
    def save_as(self, filename, **keywords):
        """
        Save the content to a new file
        """
        return save_book(self, file_name=filename, **keywords)

    def save_to_memory(self, file_type, stream=None, **keywords):
        """
        Save the content to a memory stream

        :param file_type: what format the stream is in
        :param stream: a memory stream.  Note in Python 3, for csv and tsv
                       format, please pass an instance of StringIO. For xls,
                       xlsx, and ods, an instance of BytesIO.
        """
        stream = save_book(
            self,
            file_type=file_type,
            file_stream=stream,
            **keywords,
        )
        return stream

    def save_to_django_models(
        self,
        models,
        initializers=None,
        mapdicts=None,
        **keywords,
    ):
        """
        Save to database table through django model

        :param models: a list of database models, that is accepted by
                       :meth:`Sheet.save_to_django_model`. The sequence
                       of tables matters when there is dependencies in
                       between the tables. For example, **Car** is made
                       by **Car Maker**. **Car Maker** table should be
                       specified before **Car** table.
        :param initializers: a list of intialization functions for your
                             tables and the sequence should match tables,
        :param mapdicts: custom map dictionary for your data columns
                         and the sequence should match tables

        optional parameters:
        :param batch_size: django bulk_create batch size
        :param bulk_save: whether to use bulk_create or to use single save
                          per record
        """
        save_book(
            self,
            models=models,
            initializers=initializers,
            mapdicts=mapdicts,
            **keywords,
        )

    def save_to_database(
        self,
        session,
        tables,
        initializers=None,
        mapdicts=None,
        auto_commit=True,
    ):
        """
        Save data in sheets to database tables

        :param session: database session
        :param tables: a list of database tables, that is accepted by
                       :meth:`Sheet.save_to_database`. The sequence of tables
                       matters when there is dependencies in between the
                       tables. For example, **Car** is made by **Car Maker**.
                       **Car Maker** table should
                       be specified before **Car** table.
        :param initializers: a list of intialization functions for your
                             tables and the sequence should match tables,
        :param mapdicts: custom map dictionary for your data columns
                         and the sequence should match tables
        :param auto_commit: by default, data is committed.

        """
        save_book(
            self,
            session=session,
            tables=tables,
            initializers=initializers,
            mapdicts=mapdicts,
            auto_commit=auto_commit,
        )


def _get_book(**keywords):
    """Get an instance of :class:`Book` from an excel source

    Where the dictionary should have text as keys and two dimensional
    array as values.
    """
    a_source = SOURCE.get_book_source(**keywords)
    sheets = a_source.get_data()
    filename, path = a_source.get_source_info()
    return sheets, filename, path
