# 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 os
from html import escape

NAME="Export to HTML"
VERSION="0.1"
CATEGORY=SCtg.EXPORTER

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

# main func:
def run(path, entries):
    source = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{os.path.basename(path)}</title>
<style>
{CSS}
</style>
</head>
<body>
<article class="entry">
"""
    for entry in entries:
        source += _export_entry_to_html(entry)
    source += """
</article>
</body>
</html>
"""

    with open(path, "w", encoding="utf-8") as f:
        f.write(source)

    return True


# main exporter
def _export_entry_to_html(entry):
    source = []
    list_stack = []  # [(ps_type, indent, tag)]
    ti = 0 # tag indent
    quot_type_last = VT.QT.OFF

    def _ins_tag_ind(offset_bgn, offset_end):
        nonlocal ti
        ti_bgn = ti + offset_bgn
        ti += offset_end
        return "  " * ti_bgn

    def _append_html(new_html, offset_bgn=1, offset_end=1):
        source.append(f"{_ins_tag_ind(offset_bgn, offset_end)}{new_html}")

    def _close_lists(to_indent=0):
        while list_stack and list_stack[-1][1] >= to_indent:
            tag = list_stack.pop()[2]
            _append_html(f"</{tag}>", 0, -1)

    def _close_open_tags(qt): # TODO: combine with the above one
        nonlocal quot_type_last

        # quotation
        if quot_type_last != qt:
            if quot_type_last == VT.QT.LITERARY:
                _append_html("</div>", 0, -1)
            # code
            elif quot_type_last != VT.QT.OFF:
                _append_html("</code></pre>", 0, -1)
                _append_html("</div>", 0, -1)
            quot_type_last = VT.QT.OFF


    for p in entry:
        text = p.get_text()
        indent = p.get_indent_level()
        pls = p.get_list_type()
        formats = p.get_formats()
        quot_type = p.get_quot_type()

        _close_open_tags(quot_type)

        # horizontal rule
        if p.has_hrule() and p.is_empty_completely():
            _append_html("<hr>", 1, 0)
            continue
        # TODO: find a solution for hrules with text

        # images
        if p.is_image( VT.PS.IMAGE_FILE):
            IMG_ALIGN_CLASS = {
                VT.PA.LEFT: "img-left",    # text right
                VT.PA.RIGHT: "img-right",   # text left
                VT.PA.CENTER: "img-center",  # text below
            }

            uri = p.get_uri()
            align = p.get_alignment()
            cls = IMG_ALIGN_CLASS.get(align, "img-bottom")

            _append_html(f'<div class="image-block {cls}">')
            _append_html(f'<img src="{escape(uri)}">', 1, 0)

            if text:
                _append_html(f'<div class="image-text">{_emit_inline(text, formats)}</div>', 1, 0)

            _append_html('</div>', 0, -1)
            continue
        # TODO: add support for charts and tables

        # code
        if p.is_code():
            if quot_type_last != quot_type:
                _append_html('<div class="quote-frame">')
                _append_html(f'<div class="code-header">{p.get_code_lang()}</div>', 1, 0)
                _append_html('<pre><code>')
                quot_type_last = quot_type
            source.append(f"{_emit_inline(text, formats)}") # raw append
            continue

        # headings
        heading = _get_heading_level(p)
        if heading != 0:
            _close_lists(0) # FIXME: in Lifeograph a header can as well be a list item
            _append_html(f"<h{heading}{_get_classes(p)}>{_emit_inline(text, formats)}</h{heading}>",
                        1, 0)
            continue

        # quotation
        if p.is_quote() and quot_type_last != VT.QT.LITERARY:
            _append_html('<div class="quote-frame">')
            quot_type_last = quot_type

        # lists
        tag, cls = _list_tag(pls)
        if tag:
            while list_stack and (list_stack[-1][1] > indent or
                                  (list_stack[-1][1] == indent and list_stack[-1][3] != cls)):
                _append_html(f"</{list_stack.pop()[2]}>", 0, -1)

            while not list_stack or list_stack[-1][1] < indent:
                _append_html(f"<{tag}{cls}>")
                list_stack.append((pls, indent, tag, cls))

            _append_html(f"<li>{_emit_inline(text, formats)}</li>", 1, 0)
            continue

        # normal paragraph / todo
        _close_lists(0)

        if pls in (VT.PLS.TODO, VT.PLS.PROGRESSED, VT.PLS.DONE, VT.PLS.CANCELED):
            state = {
                VT.PLS.TODO: "todo",
                VT.PLS.PROGRESSED: "progressed",
                VT.PLS.DONE: "done",
                VT.PLS.CANCELED: "canceled",
            }[pls]

            _append_html(
                f'<p class="todo indent-{indent}" data-state="{state}">'
                f'<span class="todo-marker"></span>'
                f'{_emit_inline(text, formats)}</p>', 1, 0)
        else:
            _append_html( f'<p{_get_classes(p)}>{_emit_inline(text, formats)}</p>', 1, 0)

    _close_open_tags(VT.QT.OFF)
    _close_lists(0)

    return "\n".join(source)

# inline formatting (overlap-safe)
def _emit_inline(text, formats):
    boundaries = {0, len(text)}
    for f in formats:
        boundaries.add(f.pos_bgn)
        boundaries.add(f.pos_end)

    points = sorted(boundaries)
    out = []

    for i in range(len(points) - 1):
        a, b = points[i], points[i + 1]
        if a == b:
            continue

        chunk = escape(text[a:b])
        active = [f for f in formats if f.pos_bgn <= a and f.pos_end >= b]

        # link outermost
        link = next((f for f in active if f.type == VT.FMT.LINK_URI), None)
        if link:
            chunk = f'<a href="{escape(link.uri)}">{chunk}</a>'

        for f in active:
            match f.type:
                case VT.FMT.BOLD:
                    chunk = f"<strong>{chunk}</strong>"
                case VT.FMT.ITALIC:
                    chunk = f"<em>{chunk}</em>"
                case VT.FMT.UNDERLINE:
                    chunk = f"<u>{chunk}</u>"
                case VT.FMT.STRIKETHRU:
                    chunk = f"<s>{chunk}</s>"
                case VT.FMT.HIGHLIGHT:
                    chunk = f"<mark>{chunk}</mark>"
                case VT.FMT.FADED:
                    chunk = f'<span class="faded">{chunk}</span>'
                case VT.FMT.SUPERSCRIPT:
                    chunk = f'<sup>{chunk}</sup>'
                case VT.FMT.SUBSCRIPT:
                    chunk = f'<sub>{chunk}</sub>'
                case VT.FMT.COMMENT:
                    chunk = f'<span class="comment">{chunk}</span>'
                case VT.FMT.CODE_KEYWORD:
                    chunk = f'<span class="keyword">{chunk}</span>'
                case VT.FMT.CODE_STRING:
                    chunk = f'<span class="code_string">{chunk}</span>'
                case VT.FMT.CODE_COMMENT:
                    chunk = f'<span class="comment">{chunk}</span>'
                case VT.FMT.TAG:
                    chunk = f'<span class="lifeo_tag">{chunk}</span>'

        out.append(chunk)

    return "".join(out)

# list helpers
def _list_tag(pls):
    if pls == VT.PLS.BULLET:
        return "ul", ""
    if pls == VT.PLS.NUMBER:
        return "ol", ' type="1"'
    if pls == VT.PLS.LETTER:
        return "ol", ' type="A"'
    if pls == VT.PLS.LETTER_SMALL:
        return "ol", ' type="a"'
    if pls == VT.PLS.ROMAN:
        return "ol", ' type="I"'
    if pls == VT.PLS.ROMAN_SMALL:
        return "ol", ' type="i"'
    return None, ""

# css classes for paragraph
def _get_classes(p):
    ind = p.get_indent_level()
    ind_str = f' indent-{ind}' if ind > 0 else ""
    align_str = ""
    match p.get_alignment():
        case VT.PA.CENTER:
            align_str = " align-c"
        case VT.PA.RIGHT:
            align_str = " align-r"
    return f' class="{ind_str}{align_str}"' if ind_str or align_str else ""

def _get_heading_level(p):
    match p.get_heading_level():
        case VT.PHS.TITLE:  return 1
        case VT.PHS.LARGE:  return 2
        case VT.PHS.MEDIUM: return 3
        case _: return 0

CSS = """
body {
  font-family: system-ui, sans-serif;
  line-height: 1.2;
}

.entry {
  max-width: 70ch;
  margin: 2rem auto;
}

.indent-1 { margin-left: 2em; }
.indent-2 { margin-left: 4em; }
.indent-3 { margin-left: 6em; }
.indent-4 { margin-left: 8em; }

.align-l    { text-align: left; }
.align-c    { text-align: center; }
.align-r    { text-align: right; }

.todo {
  display: flex;
  gap: .5em;
}

.todo-marker::before {
  content: "☐";
}

.todo[data-state="todo"] {
  color: red;
}

.todo[data-state="done"] .todo-marker::before {
  content: "✔";
}

.todo[data-state="progressed"] .todo-marker::before {
  content: "◐";
}

.todo[data-state="canceled"] {
  opacity: .6;
  text-decoration: line-through;
}

.faded {
  color: rgba(0, 0, 0, 0.45);
}

.comment {
  color: rgba(0, 0, 0, 0.5);
  font-size: 0.8em;
}

.lifeo_tag {
  font-family: ui-monospace, monospace;
  border: 1px solid #ccc;
  border-radius: 2px;
  background: #f0e0e0;
}

code {
  font-family: ui-monospace, monospace;
  font-size: 0.9em;
}

.quote-frame {
  border: 1px solid #ccc;
  border-radius: 6px;
  margin: 1em 0;
  background: #f8f8f8;
}

.code-header {
  font-size: 0.9em;
  padding: 4px 8px;
  background: #eaeaea;
  border-bottom: 1px solid #ccc;
  font-family: monospace;
}

.quote-frame pre {
  margin: 0;
  padding: 8px;
  overflow-x: auto;
}

.keyword {
  color: rgb(255, 0, 0);
  font-weight: bold;
}

.code_string {
  color: rgb(0, 225, 0);
}

.image-block {
  display: flex;
  gap: 12px;
  margin: 1em 0;
  align-items: flex-start;
}

.image-block img {
  max-width: 40%;
  height: auto;
}

/* text to the right of image */
.image-block.img-left {
  flex-direction: row;
}

/* text to the left of image */
.image-block.img-right {
  flex-direction: row-reverse;
}

/* text below image */
.image-block.img-center {
  flex-direction: column;
  align-items: center;
}

hr {
  border: none;
  border-top: 1px solid #ccc;
  margin: 1.5em 0;
}
"""
