# /user/bin/env python3
# -*- coding: utf-8 -*-
"""Report template class."""

import os
import os.path as pp
import re
import pkg_resources

import docx
import lxml
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.shared import Pt
from docx.oxml.ns import nsdecls, qn
from docx.oxml import OxmlElement, parse_xml
from docx.shared import Cm

from lc18report_dev.exceptions import InfoFileNotFoundError
from lc18report_dev.config import Config


class Template:
    """Template class to load and fill report template."""

    TEMPLATE_PREFIXES = {
        'lc': 'template/report_template_v',
        'lc-yuedong': 'template/yuedong_template_v',
    }

    BRK = '!_breakline_!'
    IMG = '!_img_!'

    def __init__(self, report_type='lc'):
        """Template class init function.

        Args:
            template_version (str): report template file path
            config (Config): report config object
        """
        self.config: Config = Config()
        self.version: str = ''
        self._template_file = ''
        self._template_prefix = self.TEMPLATE_PREFIXES[report_type]

    @property
    def doc(self):
        """Get doc instance from template.

        Returns:
            Document: python-docx doc instance
        """
        return docx.Document(self._template_file)

    def set_version(self, template_version: str, en_mode: bool = False) -> None:
        self.version = template_version
        postfix = '_en' if en_mode else ''
        template_file = pkg_resources.resource_filename(
            __package__, f'{self._template_prefix}{self.version}{postfix}.docx')
        config_file = pkg_resources.resource_filename(
            __package__, f'{self._template_prefix}{self.version}{postfix}.conf')
        if not pp.exists(template_file):
            raise InfoFileNotFoundError(f"模板不支持当前版本{self.version}")
        if not pp.exists(config_file):
            raise InfoFileNotFoundError(f"模板配置不支持当前版本{self.version}")
        self.set_file(template_file, config_file)

    def set_file(self, template_file: str, config_file: str) -> None:
        self._template_file = template_file
        self.config.load_config_file(config_file)

    def _swap_texts(self, texts, target_text, replace_text):
        new_texts = []
        full_text = ''.join(texts)
        match = re.search(target_text, full_text)

        if not match:
            return texts

        start = match.start()
        end = match.end()

        for text in texts:
            text_len = len(text)
            new_start = start - text_len
            new_end = end - text_len

            if end < 0:
                new_texts.append(text)

            elif start < 0:
                if new_end >= 0:
                    new_texts.append('')

                else:
                    new_texts.append(text[end:])

            else:
                if new_start >= 0:
                    new_texts.append(text)

                elif new_end >= 0:
                    new_texts.append(text[:start] + replace_text)

                else:
                    new_texts.append(text[:start] + replace_text + text[end:])

            start = new_start
            end = new_end

        new_full_text = ''.join(new_texts)
        if re.search(target_text, new_full_text):
            return self._swap_texts(
                new_full_text, target_text, replace_text)
        return new_texts

    def _extract_color(self, raw_text):
        matched = re.match(r'^#_(\w+)_#(.*)$', raw_text)
        color = None
        text = raw_text
        if matched:
            color = self.config.get_color(matched.group(1))
            text = matched.group(2)
        return color, text

    def _swap_img_paragraph(self, paragraph, target_text, replace_text):
        wrapped_target_text = r'{{' + target_text + r'}}'
        if wrapped_target_text not in paragraph.text:
            return
        img_file = replace_text.replace(self.IMG, '')
        if not os.path.isfile(img_file):
            raise FileNotFoundError(f'Image file \"{img_file}\" not found.')
        paragraph.clear()
        run = paragraph.add_run()
        run.add_picture(img_file, width=Cm(12.5))

    def _set_run_text(self, run, raw_text):
        color, text = self._extract_color(raw_text)
        if '!_breakline_!' in text:
            run.text = ''
            text_list = text.split('!_breakline_!')
            for i, text_chunk in enumerate(text_list):
                run.add_text(text_chunk)
                if i < len(text_list) - 1:
                    run.add_break()
        elif run.text != text:
            run.text = text
        if color is not None:
            run.font.color.rgb = color

    def _swap_texts_paragraph(self, paragraph, target_text, replace_text):
        if paragraph.text == '':
            return

        wrapped_target_text = r'{{' + target_text + r'}}'
        if wrapped_target_text not in paragraph.text:
            return

        if not paragraph.runs:
            return

        texts = [run.text for run in paragraph.runs]
        if not texts:
            return

        new_texts = self._swap_texts(
            texts, wrapped_target_text, replace_text)

        for i, new_text in enumerate(new_texts):
            self._set_run_text(paragraph.runs[i], new_text)
            # paragraph.runs[i].text = new_text

    def _swap_texts_paragraph_with_dict(self, paragraph, replace_dict):
        for target_text, replace_text in replace_dict.items():
            if target_text in self.config.special_items:
                continue
            try:
                if replace_text.startswith(self.IMG):
                    self._swap_img_paragraph(
                        paragraph, target_text, replace_text)
                    continue
            except AttributeError:
                pass
            self._swap_texts_paragraph(
                paragraph, target_text, replace_text)

    def _swap_texts_table_with_dict(self, table, replace_dict):
        for row in table.rows:
            for cell in row.cells:
                for paragraph in cell.paragraphs:
                    self._swap_texts_paragraph_with_dict(
                        paragraph, replace_dict)

    def _swap_texts_header_with_dict(self, doc, replace_dict):
        for section in doc.sections:
            if section.header.is_linked_to_previous:
                continue
            if not hasattr(section.header, 'paragraphs'):
                continue
            for paragraph in section.header.paragraphs:
                self._swap_texts_paragraph_with_dict(
                    paragraph, replace_dict)

    def _remove_placeholder_row(self, table):
        tbl = table._tbl
        row = table.rows[2]
        tr = row._tr
        tbl.remove(tr)

    @staticmethod
    def _set_table_cell_format(cell, table_properties):
        fill = table_properties['cell_fill']
        cell_margin = table_properties['cell_margin']
        shading_str = r'<w:shd {} w:fill="' + fill + r'"/>'
        shading_elm = parse_xml(shading_str.format(nsdecls('w')))
        cell._tc.get_or_add_tcPr().append(shading_elm)
        cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER

        tcPr = cell._tc.get_or_add_tcPr()
        tcMar = OxmlElement('w:tcMar')

        for m in ["top", "start", "bottom", "end"]:
            node = OxmlElement("w:{}".format(m))
            node.set(qn('w:w'), cell_margin)
            node.set(qn('w:type'), 'dxa')
            tcMar.append(node)

        tcPr.append(tcMar)

    @staticmethod
    def _set_table_para_format(para):
        format_str = r'<w:snapToGrid {} w:val="0" />'
        format_elm = parse_xml(format_str.format(nsdecls('w')))
        para._p.get_or_add_pPr().append(format_elm)
        para.alignment = WD_ALIGN_PARAGRAPH.LEFT

    def _add_table_row(self, table, row_content, table_properties):
        self.config.check_table_row_content(table, row_content)
        new_row = table.add_row()
        model_run = table.cell(2, 0).paragraphs[0].runs[0]
        run_style = model_run.style
        font = model_run.font
        for i, cell in enumerate(new_row.cells):
            para = cell.paragraphs[0]
            run = para.add_run()
            if isinstance(row_content[i], list):
                for j, text_chunk in enumerate(row_content[i]):
                    run.add_text(text_chunk)
                    if j < len(row_content[i]) - 1:
                        run.add_break()
            else:
                run.add_text(row_content[i])
            run.style = run_style
            run.font.name = font.name
            run.font.size = Pt(9)
            self._set_table_para_format(para)
            self._set_table_cell_format(cell, table_properties)

    def _fill_tables(self, doc, table_content):
        for table in doc.tables:
            current_table_title = table.cell(0, 0).text
            for table_title in table_content.keys():
                if not current_table_title.startswith(table_title):
                    continue
                table_properties = self.config.check_table(table)
                table_rows = table_content[table_title]
                if not table_rows:
                    # self._add_empty_table_row(table)
                    break
                for row_content in table_rows:
                    self._add_table_row(table, row_content, table_properties)
                self._remove_placeholder_row(table)

    def _add_single_multi_table(self, doc, table_name, single_table_content):
        multi_table_config = self.config.multi_tables[table_name]
        column_count = len(multi_table_config['columns']['primary'].keys())
        row_count = len(multi_table_config['columns'].keys()) + 1
        column_width = multi_table_config['properties']['width']
        style_name = multi_table_config['style']
        # Initialize table
        table = doc.add_table(row_count, column_count, style=style_name)
        # First row
        colnames = multi_table_config['columns']['primary'].keys()
        table.rows[0].height = Cm(0.76)
        for i, colname in enumerate(colnames):
            header_para = table.cell(0, i).paragraphs[0]
            header_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
            header_run = header_para.add_run()
            self._set_run_text(
                header_run, multi_table_config['columns']['primary'][colname])
            content_para = table.cell(1, i).paragraphs[0]
            content_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
            content_run = content_para.add_run()
            self._set_run_text(content_run, single_table_content[colname])
            table.cell(0, i).width = Cm(column_width[i])
            table.cell(1, i).width = Cm(column_width[i])
            # if i == 0:
            #     table.cell(0, i).width = Cm(3.5)
            #     table.cell(1, i).width = Cm(3.5)
            # if i == 1:
            #     table.cell(0, i).width = Cm(2)
            #     table.cell(1, i).width = Cm(2)
            # if i == 2:
            #     table.cell(0, i).width = Cm(5.75)
            #     table.cell(1, i).width = Cm(5.75)
            # if i == 3:
            #     table.cell(0, i).width = Cm(3.6)
            #     table.cell(1, i).width = Cm(3.6)
        if 'secondary' in multi_table_config['columns'].keys():
            # Second row
            first_cell = table.cell(2, 0)
            last_cell = table.cell(2, column_count - 1)
            span_cell = first_cell.merge(last_cell)
            content_para = span_cell.paragraphs[0]
            content_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
            content_run = content_para.add_run()
            colname = list(
                multi_table_config['columns']['secondary'].keys())[0]
            content = multi_table_config['columns']['secondary'][colname] + \
                single_table_content[colname]
            self._set_run_text(content_run, content)
        # Return table
        return table

    def _add_empty_multi_table(self, doc, table_name):
        multi_table_config = self.config.multi_tables[table_name]
        column_count = len(multi_table_config['columns']['primary'].keys())
        row_count = 2
        column_width = multi_table_config['properties']['width']
        style_name = multi_table_config['style']
        # Initialize table
        table = doc.add_table(row_count, column_count, style=style_name)
        # First row
        colnames = multi_table_config['columns']['primary'].keys()
        table.rows[0].height = Cm(0.76)
        for i, colname in enumerate(colnames):
            header_run = table.cell(0, i).paragraphs[0].add_run()
            self._set_run_text(
                header_run, multi_table_config['columns']['primary'][colname])
            table.cell(0, i).width = Cm(column_width[i])
            # if i == 0:
            #     table.cell(0, i).width = Cm(3.5)
            # if i == 1:
            #     table.cell(0, i).width = Cm(2)
            # if i == 2:
            #     table.cell(0, i).width = Cm(5.75)
            # if i == 3:
            #     table.cell(0, i).width = Cm(3.6)
        # content row
        first_cell = table.cell(1, 0)
        last_cell = table.cell(1, column_count - 1)
        span_cell = first_cell.merge(last_cell)
        content_paragraph = span_cell.paragraphs[0]
        content_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
        content_run = content_paragraph.add_run()
        content = '无检出'
        self._set_run_text(content_run, content)
        # Return table
        return table

    def _add_single_gene_table(self, doc, single_gene_table_content):
        column_count = 1
        row_count = 2
        style_name = self.config.gene_tables['style']
        # Initialize table
        table = doc.add_table(row_count, column_count, style=style_name)
        # First row
        header_para = table.cell(0, 0).paragraphs[0]
        header_run = header_para.add_run()
        self._set_run_text(header_run, '基因简介')
        content_para = table.cell(1, 0).paragraphs[0]
        content_run = content_para.add_run()
        self._set_run_text(
            content_run, single_gene_table_content['基因简介'])
        return table

    def _add_single_med_table(self, doc, single_med_table_content):
        titles = [
            '药物名称',
            '上市状态',
            '药物类型',
            '适应症(FDA)',
            '适应症(NMPA)',
            '适应症(NCCN)',
        ]
        for t, c in single_med_table_content.items():
            if not c.strip():
                titles.remove(t)
        column_count = 2
        row_count = len(titles)
        style_name = self.config.med_tables['style']
        # Initialize table
        table = doc.add_table(row_count, column_count, style=style_name)
        # Fill table content
        for i, title in enumerate(titles):
            table.cell(i, 0).width = Cm(3.5)
            table.cell(i, 1).width = Cm(11.35)
            title_para = table.cell(i, 0).paragraphs[0]
            title_run = title_para.add_run()
            self._set_run_text(title_run, title)
            content_para = table.cell(i, 1).paragraphs[0]
            content_run = content_para.add_run()
            self._set_run_text(
                content_run, single_med_table_content[title])
        return table

    @staticmethod
    def _move_table_after(table, paragraph):
        tbl, p = table._tbl, paragraph._p
        p.addnext(tbl)

    @staticmethod
    def _move_paragraph_after(paragraph_move, paragraph_target):
        p_m, p_t = paragraph_move._p, paragraph_target._p
        p_t.addnext(p_m)

    @staticmethod
    def _add_emtpy_paragraph_after(doc, paragraph):
        empty_paragraph = doc.add_paragraph()
        ep, p = empty_paragraph._p, paragraph._p
        p.addnext(ep)

    @staticmethod
    def _add_paragraph_after(doc, text, paragraph, style: str = ''):
        new_paragraph = doc.add_paragraph()
        if style:
            # style_name = 'LC18-TITLE'
            new_paragraph.style = doc.styles[style]
        new_run = new_paragraph.add_run()
        new_run.add_text(text)
        ep, p = new_paragraph._p, paragraph._p
        p.addnext(ep)

    def _fill_multi_tables(self, doc, multi_table_content):
        for paragraph in doc.paragraphs:
            tables = []
            for table_name, table_config in self.config.multi_tables.items():
                if not paragraph.text.startswith(table_config['title']):
                    continue
                for content in multi_table_content[table_config['title']]:
                    tables.append(
                        self._add_single_multi_table(doc, table_name, content))
                if not tables:
                    tables.append(self._add_empty_multi_table(doc, table_name))
                break
            tables.reverse()
            for table in tables:
                self._add_emtpy_paragraph_after(doc, paragraph)
                self._move_table_after(table, paragraph)

    def _fill_appendix_tables(
            self, doc,
            gene_table_content: dict,
            med_table_content: dict):
        paragraph_target = None

        for paragraph in doc.paragraphs:
            if paragraph.text.startswith('附录2'):
                paragraph_target = paragraph
                break

        if paragraph_target is None:
            return

        if any(content is not None for content in med_table_content.values()):
            ps_msg = '注：此处只列举变异相关提示敏感的A级药物。'
            self._add_paragraph_after(
                doc, ps_msg, paragraph_target, style='LC18-TEXT')
            meds = list(med_table_content.keys())
            meds.reverse()
            for med in meds:
                single_med_table_content = med_table_content[med]
                if single_med_table_content is None:
                    continue
                table = self._add_single_med_table(
                    doc, single_med_table_content)
                self._move_table_after(table, paragraph_target)
                self._add_emtpy_paragraph_after(doc, paragraph_target)
            self._add_paragraph_after(
                doc, '相关药物', paragraph_target, style='LC18-TITLE')
            self._add_emtpy_paragraph_after(doc, paragraph_target)

        genes = list(gene_table_content.keys())
        if len(genes) == 0:
            self._add_paragraph_after(
                doc, '无检出', paragraph_target, style='LC18-TITLE')
            return
        genes.reverse()
        for gene in genes:
            single_gene_table_content = gene_table_content[gene]
            if single_gene_table_content is None:
                continue
            table = self._add_single_gene_table(doc, single_gene_table_content)
            self._move_table_after(table, paragraph_target)
            self._add_paragraph_after(
                doc, gene, paragraph_target, style='LC18-TITLE')
            self._add_emtpy_paragraph_after(doc, paragraph_target)

    def make_report(self, output_path, replace_dict) -> None:
        """Make report with report content.

        Args:
            output_path (str): output report path
            replace_dict (dict): report content dict
        """
        doc = self.doc
        for paragraph in doc.paragraphs:
            self._swap_texts_paragraph_with_dict(paragraph, replace_dict)
        for table in doc.tables:
            self._swap_texts_table_with_dict(table, replace_dict)
        self._swap_texts_header_with_dict(doc, replace_dict)
        if 'tables' in replace_dict.keys():
            self._fill_tables(doc, replace_dict['tables'])
        if 'multi_tables' in replace_dict.keys():
            self._fill_multi_tables(doc, replace_dict['multi_tables'])
        if all(t in replace_dict.keys() for t in ['gene_tables', 'med_tables']):
            self._fill_appendix_tables(
                doc, replace_dict['gene_tables'], replace_dict['med_tables'])
        namespace = self.config.namespace
        element_updatefields = lxml.etree.SubElement(
            doc.settings.element, f'{namespace}updateFields')
        element_updatefields.set(f'{namespace}val', 'true')
        doc.save(output_path)
