import argparse
from pathlib import Path

from xl2htm import xls
from xl2htm import xlsx


class Merge:
    def __init__(self, start_row, start_col, end_row, end_col):
        self.start_row = start_row
        self.start_col = start_col
        self.end_row = end_row
        self.end_col = end_col


class Cell:
    def __init__(self, row_num, col_num, value):
        self.row_num = row_num
        self.col_num = col_num
        self.value = value

    def __repr__(self):
        return f"<{self.__class__.__module__}.{self.__class__.__name__}({self.row_num},{self.col_num})={self.value!r}>"

    col_names = (
        [""]
        + [chr(ord("A") + i) for i in range(26)]
        + ["A" + chr(ord("A") + i) for i in range(26)]
    )

    def col_name(self):
        return self.__class__.col_names[self.col_num]


class Sheet:
    def __init__(self, name, grid, merged_cells, hidden):
        self.name = name
        self.grid = grid
        self.grid = self.cut_grid()
        self.merged_cells = merged_cells
        self.hidden = hidden

    def cut_grid(self):
        max_filled_column = 0
        max_filled_row = 0
        for cell in self.cells():
            if cell.value is not None:
                if cell.col_num > max_filled_column:
                    max_filled_column = cell.col_num
                if cell.row_num > max_filled_row:
                    max_filled_row = cell.row_num
        return [
            row[0 : (max_filled_column + 1)]
            for row in self.grid[0 : (max_filled_row + 1)]
        ]

    def grid_values(self):
        return [[cell.value for cell in row] for row in self.grid]

    def cells(self):
        for row in self.grid:
            for cell in row:
                yield cell

    def columns_count(self):
        lens = [len(row) for row in self.grid]
        return max(lens) if len(lens) > 0 else 0

    def rows_count(self):
        return len(self.grid)

    def escape_html(self, value):
        """Escape HTML special characters."""
        if value is None:
            return ""
        value = str(value)
        return (
            value.replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace('"', "&quot;")
            .replace("'", "&#39;")
        )

    def esc_name(self):
        return self.escape_html(self.name)

    def table(self):
        # Get hidden rows and columns
        hidden_rows, hidden_cols = self.hidden

        # Create a 2D array to track merged cells
        merged_map = {}
        for merge in self.merged_cells:
            for r in range(merge.start_row, merge.end_row + 1):
                for c in range(merge.start_col, merge.end_col + 1):
                    merged_map[(r, c)] = merge

        html = ["<table border='1'>"]

        for row in self.grid:
            if row[0].row_num in hidden_rows:
                for cell in row:
                    if (cell.row_num, cell.col_num) in merged_map:
                        merge = merged_map[(cell.row_num, cell.col_num)]
                        if (
                            cell.row_num == merge.start_row
                            and cell.col_num == merge.start_col
                        ):
                            if len(self.grid) > cell.row_num:
                                next_row = self.grid[cell.row_num]
                                next_row[cell.col_num - 1].value = cell.value
                                merge.start_row = cell.row_num + 1
                continue

            html.append(f"<tr class='row-{row[0].row_num}'>")
            for cell in row:
                if cell.col_num in hidden_cols:
                    if (cell.row_num, cell.col_num) in merged_map:
                        merge = merged_map[(cell.row_num, cell.col_num)]
                        if (
                            cell.row_num == merge.start_row
                            and cell.col_num == merge.start_col
                        ):
                            if len(row) > cell.col_num:
                                next_cell = row[cell.col_num]
                                next_cell.value = cell.value
                                merge.start_col = cell.col_num + 1
                    continue

                if (cell.row_num, cell.col_num) in merged_map:
                    merge = merged_map[(cell.row_num, cell.col_num)]
                    if (
                        cell.row_num == merge.start_row
                        and cell.col_num == merge.start_col
                    ):
                        rowspan = sum(
                            1
                            for num in range(merge.start_row, merge.end_row + 1)
                            if num not in hidden_rows
                        )
                        colspan = sum(
                            1
                            for num in range(merge.start_col, merge.end_col + 1)
                            if num not in hidden_cols
                        )
                        value = self.escape_html(cell.value)
                        attrs = [f"class='col-{cell.col_name()}'"]
                        if rowspan > 1:
                            attrs.append(f"rowspan='{rowspan}'")
                        if colspan > 1:
                            attrs.append(f"colspan='{colspan}'")
                        attr_str = " ".join(attrs)
                        html.append(f"<td {attr_str}>{value}</td>")
                    continue

                value = self.escape_html(cell.value)
                html.append(f"<td class='col-{cell.col_name()}'>{value}</td>")
            html.append("</tr>")

        html.append("</table>")
        return "".join(html)


def extract_sheets(path, names=[]):
    lower_path = path.lower()
    if lower_path.endswith("xlsx"):
        mod = xlsx
    elif lower_path.endswith("xls"):
        mod = xls
    else:
        raise ValueError("Could not read this type of data")

    sheets_dict = {}
    for name, sheet in mod.sheets(path):
        if names and name not in names:
            continue
        grid = [[Cell(*cell) for cell in row] for row in mod.grid(sheet)]
        merges = [Merge(*m) for m in mod.merges(sheet)]
        hidden_rows = mod.hidden_rows(sheet)
        hidden_cols = mod.hidden_cols(sheet)
        sh = Sheet(name, grid, merges, (hidden_rows, hidden_cols))
        sheets_dict[name] = sh

    if not sheets_dict:
        raise ValueError("No Sheet found")

    if names:
        sheets = [sheets_dict[name] for name in names if name in sheets_dict]
    else:
        sheets = list(sheets_dict.values())

    return sheets


def extract_sheet(path, name):
    return extract_sheets(path, [name])[0]


def excel_to_html(excel_path, sheet_names=None):
    sheets = extract_sheets(excel_path, sheet_names)

    html = []
    html.append(
        f"""<!DOCTYPE html>
<html>
<head>
    <meta charset='UTF-8'>
    <title>{Path(excel_path).stem}</title>
    <style>
        h1 {{ border-bottom: 2px solid; }}
        table {{ border-collapse: collapse; }}
        .t {{ margin-bottom: 40px; }}
    </style>
</head>
<body>"""
    )

    for sh in sheets:
        html.append(f"<div class='t'><h1>{sh.esc_name()}</h1>{sh.table()}</div>")

    html.append("</body></html>")

    return "".join(html)


def main():
    parser = argparse.ArgumentParser(
        description="Convert Excel file to HTML (Support merged cells and hidden rows/columns)",
        usage="xl2htm excel_file [-s SHEETS [SHEETS ...]]",
    )
    parser.add_argument("excel_file")
    parser.add_argument("-s", "--sheets", nargs="+")

    args = parser.parse_args()

    print(excel_to_html(args.excel_file, args.sheets))


if __name__ == "__main__":
    main()
