# Copyright (C) 2007-2026 Ahmet Öztürk (aoz_2@yahoo.com)
#
# This file is part of Lifeograph.
#
# Lifeograph is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Lifeograph is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Lifeograph.  If not, see <http://www.gnu.org/licenses/>.

from LoG import *
import docx
from docx.shared import Inches, RGBColor, Pt
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
from urllib.parse import urlparse, unquote

NAME="Export Docx"
VERSION="0.1"
CATEGORY=SCtg.EXPORTER

# get the default extension:
def get_ext():
    return "docx"

# main func:
def run(path, entries):
    doc = docx.Document()
    disable_page_break = True
    for entry in entries:
        _export_entry_to_docx(doc, entry, disable_page_break)
        disable_page_break = False
    doc.save(path)
    return True


def _export_entry_to_docx(doc, entry, disable_page_break):
    # list styles
    LIST_DEFS = {
        VT.PLS.BULLET:       "bullet",
        VT.PLS.NUMBER:       "decimal",
        VT.PLS.LETTER:       "upperLetter",
        VT.PLS.LETTER_SMALL: "lowerLetter",
        VT.PLS.ROMAN:        "upperRoman",
        VT.PLS.ROMAN_SMALL:  "lowerRoman",
    }
    num_ids = {}
    for k, fmt in LIST_DEFS.items():
        num_ids[k] = _create_numbering(doc, fmt)

    # paragraph loop
    for p in entry:
        text = p.get_text()

        # horizontal rules
        if len(text) == 0 and p.has_hrule():
            _add_horizontal_rule(doc)
            continue

        # images
        if p.is_image(VT.PS.IMAGE_FILE):
            _add_image_paragraph(doc, p)
            continue
        # TODO: add support for charts and tables

        # headings
        heading = p.get_heading_level()
        match heading:
            case VT.PHS.TITLE:
                if not disable_page_break:
                    doc.add_page_break()
                para_w = doc.add_heading(level=1)
            case VT.PHS.LARGE:
                para_w = doc.add_heading(level=2)
            case VT.PHS.MEDIUM:
                para_w = doc.add_heading(level=3)
            case _:
                para_w = doc.add_paragraph()

        if p.get_quot_type() != VT.QT.OFF:
            _set_para_quote(para_w)

        # alignment
        _apply_alignment(para_w, p.get_alignment())

        # list styles
        list_type = p.get_list_type()
        indent = p.get_indent_level()
        match list_type:
            case (VT.PLS.BULLET | VT.PLS.NUMBER | VT.PLS.LETTER | VT.PLS.LETTER_SMALL |
                  VT.PLS.ROMAN | VT.PLS.ROMAN_SMALL):
                _apply_list(para_w, p.get_list_type(), p.get_indent_level(), num_ids)
                indent = 0
            case VT.PLS.TODO:
                para_w.add_run("⏹️ ")
            case VT.PLS.PROGRESSED:
                para_w.add_run("🔄 ")
            case VT.PLS.DONE:
                para_w.add_run("✅ ")
            case VT.PLS.CANCELED:
                para_w.add_run("❌ ")

        # indentation
        if indent > 0:
            para_w.paragraph_format.left_indent = Inches(0.3 * indent)

        # formats & text
        formats = p.get_formats()
        _apply_text_with_formats(para_w, text, formats, list_type, p.get_quot_type() != VT.QT.OFF)


def _apply_text_with_formats(para, text, formats, ps, is_code):
    def _add_hyperlink(paragraph, text, url):
        part = paragraph.part
        r_id = part.relate_to(url, RT.HYPERLINK, is_external=True)

        # create real run
        run = paragraph.add_run(text)

        # now wrap the run in a hyperlink
        hyperlink = OxmlElement("w:hyperlink")
        hyperlink.set(qn("r:id"), r_id)

        hyperlink.append(run._r)

        paragraph._p.append(hyperlink)

        return run


    boundaries = {0, len(text)}
    for f in formats:
        boundaries.add(f.pos_bgn)
        boundaries.add(f.pos_end)
    boundaries = sorted(boundaries)

    for i in range(len(boundaries) - 1):
        bgn, end = boundaries[i], boundaries[i + 1]
        if bgn == end:
            continue

        span = text[bgn:end]
        active_formats = [f for f in formats if f.pos_bgn <= bgn and f.pos_end >= end]

        link = next((f for f in active_formats if f.type == VT.FMT.LINK_URI), None)
        # TODO: add links to LINK_ONTHEFLYs when uri does not start with file://

        # create run
        if is_code:
            run = _add_code_run(para, span)
        elif link:
            run = _add_hyperlink(para, span, link.uri)
        else:
            run = para.add_run(span)

        # hard reset (critical)
        run.font.bold = False
        run.font.italic = False
        run.font.underline = bool(link)
        run.font.strike = False
        run.font.subscript = False
        run.font.superscript = False
        run.font.highlight_color = None

        # styles from list type
        match ps:
            case VT.PLS.TODO:
                run.font.color.rgb = RGBColor(230, 33, 33)
            case VT.PLS.DONE:
                run.font.highlight_color = docx.enum.text.WD_COLOR_INDEX.BRIGHT_GREEN
            case VT.PLS.CANCELED:
                run.font.strike = True

        # apply formats
        for f in active_formats:
            match f.type:
                case VT.FMT.BOLD:
                    run.bold = True
                case VT.FMT.ITALIC:
                    run.italic = True
                case VT.FMT.UNDERLINE:
                    run.underline = True
                case VT.FMT.STRIKETHRU:
                    run.font.strike = True
                case VT.FMT.HIGHLIGHT:
                    run.font.highlight_color = docx.enum.text.WD_COLOR_INDEX.YELLOW
                case VT.FMT.FADED:
                    run.font.color.rgb = RGBColor(128, 128, 128)
                case VT.FMT.SUPERSCRIPT:
                    run.font.superscript = True
                    run.font.subscript = False
                case VT.FMT.SUBSCRIPT:
                    run.font.subscript = True
                    run.font.superscript = False
                case VT.FMT.COMMENT:
                    run.font.color.rgb = RGBColor(128, 128, 128)
                    run.font.size = Pt(8)
                case VT.FMT.CODE_KEYWORD:
                    run.font.color.rgb = RGBColor(255, 0, 0)
                    run.bold = True
                case VT.FMT.CODE_STRING:
                    run.font.color.rgb = RGBColor(64, 224, 64)
                case VT.FMT.CODE_COMMENT:
                    run.font.color.rgb = RGBColor(128, 128, 128)
                case VT.FMT.TAG:
                    run.font.name = "Consolas"
                    run.font.highlight_color = docx.enum.text.WD_COLOR_INDEX.GRAY_25
                case VT.FMT.LINK_URI | VT.FMT.LINK_EVAL | VT.FMT.LINK_ONTHEFLY:
                    run.font.color.rgb = RGBColor(0, 0, 255)

def _add_horizontal_rule(document, thickness=6):
    p = document.add_paragraph()
    p_pr = p._p.get_or_add_pPr()

    p_bdr = OxmlElement("w:pBdr")

    bottom = OxmlElement("w:bottom")
    bottom.set(qn("w:val"), "single")
    bottom.set(qn("w:sz"), str(thickness))   # line thickness
    bottom.set(qn("w:space"), "1")
    bottom.set(qn("w:color"), "auto")

    p_bdr.append(bottom)
    p_pr.append(p_bdr)

def _apply_alignment(para_w, align):
    match align:
        case VT.PA.CENTER:
            para_w.alignment = WD_ALIGN_PARAGRAPH.CENTER
        case VT.PA.RIGHT:
            para_w.alignment = WD_ALIGN_PARAGRAPH.RIGHT
        case _:
            para_w.alignment = WD_ALIGN_PARAGRAPH.LEFT

def _create_numbering(document, num_fmt):
    numbering = document.part.numbering_part.element

    # next available IDs
    abstract_ids = [
        int(n.get(qn("w:abstractNumId")))
        for n in numbering.findall(qn("w:abstractNum"))
    ]
    abstract_id = max(abstract_ids, default=-1) + 1

    num_ids = [
        int(n.get(qn("w:numId")))
        for n in numbering.findall(qn("w:num"))
    ]
    num_id = max(num_ids, default=-1) + 1

    # abstract numbering
    abstract = OxmlElement("w:abstractNum")
    abstract.set(qn("w:abstractNumId"), str(abstract_id))

    for level in range(0, 9):
        lvl = OxmlElement("w:lvl")
        lvl.set(qn("w:ilvl"), str(level))

        # numbering format
        fmt = OxmlElement("w:numFmt")
        fmt.set(qn("w:val"), num_fmt)

        # numbering text (always %1., %2., etc.)
        txt = OxmlElement("w:lvlText")
        txt.set(qn("w:val"), f"%{level + 1}. ")

        # 🔧 critical: avoid Word's default TAB
        suff = OxmlElement("w:suff")
        suff.set(qn("w:val"), "space")

        lvlJc = OxmlElement("w:lvlJc")
        lvlJc.set(qn("w:val"), "right")

        # paragraph properties
        pPr = OxmlElement("w:pPr")
        ind = OxmlElement("w:ind")

        # values in twips
        left = 720 + level * 360   # grows per nesting
        hanging = 360              # keeps marker–text tight

        ind.set(qn("w:left"), str(left))
        ind.set(qn("w:hanging"), str(hanging))

        pPr.append(ind)

        lvl.extend([fmt, txt, suff, lvlJc, pPr])
        abstract.append(lvl)

    numbering.append(abstract)

    # concrete numbering
    num = OxmlElement("w:num")
    num.set(qn("w:numId"), str(num_id))

    abs_id = OxmlElement("w:abstractNumId")
    abs_id.set(qn("w:val"), str(abstract_id))

    num.append(abs_id)
    numbering.append(num)

    return num_id

def _apply_list(para_w, list_type, indent, num_ids):
    if not list_type:
        return  # not a list paragraph

    if list_type not in num_ids:
        return  # unsupported / unknown list type

    pPr = para_w._p.get_or_add_pPr()

    numPr = OxmlElement("w:numPr")

    ilvl = OxmlElement("w:ilvl")
    ilvl.set(qn("w:val"), str(indent))

    numId = OxmlElement("w:numId")
    numId.set(qn("w:val"), str(num_ids[list_type]))

    numPr.append(ilvl)
    numPr.append(numId)
    pPr.append(numPr)

def _set_para_quote(para_w):
    # paragraph shading (light gray)
    pPr = para_w._p.get_or_add_pPr()
    shd = OxmlElement("w:shd")
    shd.set(qn("w:val"), "clear")
    shd.set(qn("w:color"), "auto")
    shd.set(qn("w:fill"), "EDEDED")
    pPr.append(shd)

    # spacing before/after
    para_w.paragraph_format.space_before = Pt(6)
    para_w.paragraph_format.space_after = Pt(6)

def _add_code_run(para_w, text):
    # para_w.style = "Normal"

    run = para_w.add_run(text)

    # monospace
    run.font.name = "Consolas"
    run.font.size = Pt(10)

    # prevent ligatures / smart substitutions
    run.font._element.rPr.append(OxmlElement("w:noProof"))

    return run


def _add_image_paragraph(doc, para):
    path = _uri_to_path(para.get_uri())
    # text = para.get_text().strip()
    align = para.get_alignment()

    if align == VT.PS.ALIGN_C:
        table = doc.add_table(rows=2, cols=1)
        img_cell = table.cell(0, 0)
        txt_cell = table.cell(1, 0)

    elif align == VT.PS.ALIGN_R:
        table = doc.add_table(rows=1, cols=2)
        txt_cell = table.cell(0, 0)
        img_cell = table.cell(0, 1)

    else:  # ALIGN_L (default)
        table = doc.add_table(rows=1, cols=2)
        img_cell = table.cell(0, 0)
        txt_cell = table.cell(0, 1)

    table.alignment = WD_TABLE_ALIGNMENT.CENTER

    _insert_image_into_cell(img_cell, path)
    _insert_text_into_cell(txt_cell, para)

def _insert_image_into_cell(cell, path):
    para_w = cell.paragraphs[0]
    run = para_w.add_run()
    run.add_picture(path, width=Inches(3))
    para_w.alignment = WD_ALIGN_PARAGRAPH.CENTER
    cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER

def _insert_text_into_cell(cell, para):
    para_w = cell.paragraphs[0]
    match para.get_alignment():
        case VT.PS.ALIGN_L:
            para_w.alignment = WD_ALIGN_PARAGRAPH.LEFT
        case VT.PS.ALIGN_C:
            para_w.alignment = WD_ALIGN_PARAGRAPH.CENTER
        case VT.PS.ALIGN_R:
            para_w.alignment = WD_ALIGN_PARAGRAPH.RIGHT

    if para.get_text():
        _apply_text_with_formats(para_w, para.get_text(), para.get_formats(),
                                 para.get_list_type(), False)

def _uri_to_path(uri: str) -> str:
    parsed = urlparse(uri)
    return unquote(parsed.path)
