from typing import Optional, Dict, List, Callable

from django.utils.html import escape


class Cell:
    def __init__(
        self,
        value: str,
        is_header: bool = False,
        title: Optional[str] = None,
        classes: Optional[str] = None,
        extra_data: Optional[Dict] = None,
    ):
        self.value = value
        self.is_header = is_header
        self.title = title
        self.classes = classes
        self.extra_data = extra_data

    def render(self) -> str:
        result = ""

        if self.is_header:
            result += "<th"
        else:
            result += "<td"
        title = self.render_title()
        if title is not None:
            result += ' title="' + title + '"'
        classes = self.render_classes()
        if classes is not None:
            result += ' class="' + classes + '"'
        result += ">"

        result += self.render_inner()

        if self.is_header:
            result += "</th>"
        else:
            result += "</td>"
        return result

    def render_inner(self) -> str:
        if (
            self.extra_data is not None
            and self.extra_data.get("Cell_render_inner") is not None
        ):
            return self.extra_data.get("Cell_render_inner")(
                self.value,
                self.extra_data,
            )
        return escape(str(self.value))

    def render_title(self) -> str:
        if (
            self.extra_data is not None
            and self.extra_data.get("Cell_render_title") is not None
        ):
            return self.extra_data.get("Cell_render_title")(
                self.value,
                self.title,
                self.extra_data,
            )
        if self.title is None:
            return None
        return escape(str(self.title))

    def render_classes(self) -> str:
        if (
            self.extra_data is not None
            and self.extra_data.get("Cell_render_classes") is not None
        ):
            return self.extra_data.get("Cell_render_classes")(
                self.value,
                self.classes,
                self.extra_data,
            )
        if self.classes is None:
            return None
        return escape(str(self.classes))



class Row:
    def __init__(
        self,
        cells: List[Cell],
        title: Optional[str] = None,
        classes: Optional[str] = None,
        extra_data: Optional[Dict] = None,
    ):
        self.cells = cells
        self.title = title
        self.classes = classes
        self.extra_data = extra_data

    def render(self) -> str:
        result = "<tr"
        title = self.render_title()
        if title is not None:
            result += ' title="' + title + '"'
        classes = self.render_classes()
        if classes is not None:
            result += ' class="' + classes + '"'
        result += ">"

        result += self.render_inner()

        result += "</tr>"
        return result

    def render_inner(self) -> str:
        result = ""
        for cell in self.cells:
            result += cell.render()
        return result

    def render_title(self) -> str:
        if (
            self.extra_data is not None
            and self.extra_data.get("Row_render_title") is not None
        ):
            return self.extra_data.get("Row_render_title")(
                self.title,
                self.extra_data,
            )
        if self.title is None:
            return None
        return escape(str(self.title))

    def render_classes(self) -> str:
        if (
            self.extra_data is not None
            and self.extra_data.get("Row_render_classes") is not None
        ):
            return self.extra_data.get("Row_render_classes")(
                self.classes,
                self.extra_data,
            )
        if self.classes is None:
            return None
        return escape(str(self.classes))



class TableHeadOrBody:
    def __init__(
        self,
        rows: List[Row],
        is_head: bool = False,
        classes: Optional[str] = None,
        extra_data: Optional[Dict] = None,
    ):
        self.rows = rows
        self.is_head = is_head
        self.classes = classes
        self.extra_data = extra_data

    def render(self) -> str:
        result = ""
        if self.is_head:
            result += "<thead"
        else:
            result += "<tbody"
        classes = self.render_classes()
        if classes is not None:
            result += ' class="' + classes + '"'
        result += ">"

        result += self.render_inner()

        if self.is_head:
            result += "</thead>"
        else:
            result += "</tbody>"
        return result

    def render_inner(self) -> str:
        result = ""
        for row in self.rows:
            result += row.render()
        return result

    def render_classes(self) -> str:
        if (
            self.extra_data is not None
            and self.extra_data.get("TableHeadOrBody_render_classes") is not None
        ):
            return self.extra_data.get("TableHeadOrBody_render_classes")(
                self.classes,
                self.extra_data,
            )
        if self.classes is None:
            return None
        return escape(str(self.classes))



class Table:
    def __init__(
        self,
        table_head: Optional[TableHeadOrBody],
        table_body: Optional[TableHeadOrBody],
        classes: Optional[str] = None,
        extra_data: Optional[Dict] = None,
    ):
        self.table_head = table_head
        self.table_body = table_body
        self.classes = classes
        self.extra_data = extra_data

    def render(self) -> str:
        result = "<table"
        classes = self.render_classes()
        if classes is not None:
            result += ' class="' + classes + '"'
        result += ">"

        result += self.render_inner()

        result += "</table>"
        return result

    def render_inner(self) -> str:
        result = ""
        if self.table_head is not None:
            result += self.table_head.render()
        if self.table_body is not None:
            result += self.table_body.render()
        return result

    def render_classes(self) -> str:
        if (
            self.extra_data is not None
            and self.extra_data.get("Table_render_classes") is not None
        ):
            return self.extra_data.get("Table_render_classes")(
                self.classes,
                self.extra_data,
            )
        if self.classes is None:
            return None
        return escape(str(self.classes))



class Column:
    def __init__(
        self,
        header_name: str,
        attribute_name: str,
        header_Cell_render_inner: Optional[Callable] = None,
        header_Cell_render_title: Optional[Callable] = None,
        header_Cell_render_classes: Optional[Callable] = None,
        body_Cell_render_inner: Optional[Callable] = None,
        body_Cell_render_title: Optional[Callable] = None,
        body_Cell_render_classes: Optional[Callable] = None,
        header_title: Optional[str] = None,
        header_classes: Optional[str] = None,
        body_title: Optional[str] = None,
        body_classes: Optional[str] = None,
    ):
        self.header_name = header_name
        self.attribute_name = attribute_name
        self.header_Cell_render_inner = header_Cell_render_inner
        self.header_Cell_render_title = header_Cell_render_title
        self.header_Cell_render_classes = header_Cell_render_classes
        self.body_Cell_render_inner = body_Cell_render_inner
        self.body_Cell_render_title = body_Cell_render_title
        self.body_Cell_render_classes = body_Cell_render_classes
        self.header_title = header_title
        self.header_classes = header_classes
        self.body_title = body_title
        self.body_classes = body_classes

    def _get_header_extra_data(self, extra_data: Optional[Dict]) -> Optional[Dict]:
        if self.header_Cell_render_inner is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["Cell_render_inner"] = self.header_Cell_render_inner
        if self.header_Cell_render_title is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["Cell_render_title"] = self.header_Cell_render_title
        if self.header_Cell_render_classes is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["Cell_render_classes"] = self.header_Cell_render_classes
        return extra_data

    def get_header_cell(self, extra_data: Optional[Dict] = None) -> Cell:
        return Cell(
            value=self.header_name,
            is_header=True,
            title=self.header_title,
            classes=self.header_classes,
            extra_data=self._get_header_extra_data(extra_data),
        )

    def _get_body_extra_data(
        self,
        extra_data: Optional[Dict],
        data_or_object,
    ) -> Optional[Dict]:
        if extra_data is None:
            extra_data = {}
        extra_data["row"] = data_or_object
        if self.body_Cell_render_inner is not None:
            extra_data["Cell_render_inner"] = self.body_Cell_render_inner
        if self.body_Cell_render_title is not None:
            extra_data["Cell_render_title"] = self.body_Cell_render_title
        if self.body_Cell_render_classes is not None:
            extra_data["Cell_render_classes"] = self.body_Cell_render_classes
        return extra_data

    def get_body_cell(self, data_or_object, extra_data: Optional[Dict] = None) -> Cell:
        if hasattr(data_or_object, self.attribute_name):
            value = getattr(data_or_object, self.attribute_name)
        else:
            value = data_or_object.get(self.attribute_name)
        return Cell(
            value=value,
            is_header=False,
            title=self.body_title,
            classes=self.body_classes,
            extra_data=self._get_body_extra_data(extra_data, data_or_object),
        )



class TableDefinition:
    def __init__(
        self,
        columns: List[Column],
        data_list: List,
        header_row_title: Optional[str] = None,
        header_row_classes: Optional[str] = None,
        header_Row_render_title: Optional[Callable] = None,
        header_Row_render_classes: Optional[Callable] = None,
        body_row_title: Optional[str] = None,
        body_row_classes: Optional[str] = None,
        body_Row_render_title: Optional[Callable] = None,
        body_Row_render_classes: Optional[Callable] = None,
        header_classes: Optional[str] = None,
        header_TableHeadOrBody_render_classes: Optional[Callable] = None,
        body_classes: Optional[str] = None,
        body_TableHeadOrBody_render_classes: Optional[Callable] = None,
        table_classes: Optional[str] = None,
        Table_render_classes: Optional[Callable] = None,
    ):
        self.columns = columns
        self.data_list = data_list
        self.header_row_title = header_row_title
        self.header_row_classes = header_row_classes
        self.header_Row_render_title = header_Row_render_title
        self.header_Row_render_classes = header_Row_render_classes
        self.body_row_title = body_row_title
        self.body_row_classes = body_row_classes
        self.body_Row_render_title = body_Row_render_title
        self.body_Row_render_classes = body_Row_render_classes
        self.header_classes = header_classes
        self.header_TableHeadOrBody_render_classes = (
            header_TableHeadOrBody_render_classes
        )
        self.body_classes = body_classes
        self.body_TableHeadOrBody_render_classes = body_TableHeadOrBody_render_classes
        self.table_classes = table_classes
        self.Table_render_classes = Table_render_classes

    def _get_header_row_extra_data(self, extra_data: Optional[Dict]) -> Optional[Dict]:
        if self.header_Row_render_title is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["Row_render_title"] = self.header_Row_render_title
        if self.header_Row_render_classes is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["Row_render_classes"] = self.header_Row_render_classes
        return extra_data

    def _get_table_header_extra_data(
        self,
        extra_data: Optional[Dict],
    ) -> Optional[Dict]:
        if self.header_TableHeadOrBody_render_classes is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["TableHeadOrBody_render_classes"] = (
                self.header_TableHeadOrBody_render_classes
            )
        return extra_data

    def _get_table_head(self) -> TableHeadOrBody:
        header_cells = []
        for column in self.columns:
            header_cells.append(column.get_header_cell())
        header_row = Row(
            header_cells,
            title=self.header_row_title,
            classes=self.header_row_classes,
            extra_data=self._get_header_row_extra_data(None),
        )
        table_head = TableHeadOrBody(
            [header_row],
            is_head=True,
            classes=self.header_classes,
            extra_data=self._get_table_header_extra_data(None),
        )
        return table_head

    def _get_body_row_extra_data(self, extra_data: Optional[Dict]) -> Optional[Dict]:
        if self.body_Row_render_title is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["Row_render_title"] = self.body_Row_render_title
        if self.body_Row_render_classes is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["Row_render_classes"] = self.body_Row_render_classes
        return extra_data

    def _get_table_body_extra_data(self, extra_data: Optional[Dict]) -> Optional[Dict]:
        if self.body_TableHeadOrBody_render_classes is not None:
            if extra_data is None:
                extra_data = {}
            extra_data["TableHeadOrBody_render_classes"] = (
                self.body_TableHeadOrBody_render_classes
            )
        return extra_data

    def _get_table_body(self) -> TableHeadOrBody:
        body_rows = []
        for data in self.data_list:
            body_cells = []
            for column in self.columns:
                body_cells.append(column.get_body_cell(data))
            body_row = Row(
                body_cells,
                title=self.body_row_title,
                classes=self.body_row_classes,
                extra_data=self._get_body_row_extra_data(None),
            )
            body_rows.append(body_row)
        table_body = TableHeadOrBody(
            body_rows,
            is_head=False,
            classes=self.body_classes,
            extra_data=self._get_table_body_extra_data(None),
        )
        return table_body

    def _get_table_extra_data(self, extra_data: Optional[Dict]) -> Optional[Dict]:
        if self.Table_render_classes is not None:
            extra_data["Table_render_classes"] = (
                self.Table_render_classes
            )
        return extra_data

    def get_table(self) -> Table:
        table_head = self._get_table_head()
        table_body = self._get_table_body()
        return Table(
            table_head=table_head,
            table_body=table_body,
            classes=self.table_classes,
            extra_data=self._get_table_extra_data({}),
        )