#!/usr/bin/env python
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>

import numbers
from collections import OrderedDict


class Inherit:
    def __eq__(self, other):
        return other is self

    def __hash__(self):
        return id(self)

    def __lt__(self, other):
        return False

    def __gt__(self, other):
        return other is not self

    def __ge__(self, other):
        if self is other:
            return True
        return True

    def __le__(self, other):
        if self is other:
            return True
        return False


inherit = Inherit()


def binary_property(parent, name, XPath, get):
    vals = XPath(f'./w:{name}')(parent)
    if not vals:
        return inherit
    val = get(vals[0], 'w:val', 'on')
    return True if val in {'on', '1', 'true'} else False


def simple_color(col, auto='currentColor'):
    if not col or col == 'auto' or len(col) != 6:
        return auto
    return '#' + col


def simple_float(val, mult=1.0):
    try:
        return float(val) * mult
    except ValueError, TypeError, AttributeError, KeyError:
        pass


def twips(val, mult=0.05):
    """Parse val as either a pure number representing twentieths of a point or a number followed by the suffix pt, representing pts."""
    try:
        return float(val) * mult
    except ValueError, TypeError, AttributeError, KeyError:
        if val and val.endswith('pt') and mult == 0.05:
            return twips(val[:-2], mult=1.0)


LINE_STYLES = {  # {{{
    'basicBlackDashes': 'dashed',
    'basicBlackDots': 'dotted',
    'basicBlackSquares': 'dashed',
    'basicThinLines': 'solid',
    'dashDotStroked': 'groove',
    'dashed': 'dashed',
    'dashSmallGap': 'dashed',
    'dotDash': 'dashed',
    'dotDotDash': 'dashed',
    'dotted': 'dotted',
    'double': 'double',
    'inset': 'inset',
    'nil': 'none',
    'none': 'none',
    'outset': 'outset',
    'single': 'solid',
    'thick': 'solid',
    'thickThinLargeGap': 'double',
    'thickThinMediumGap': 'double',
    'thickThinSmallGap': 'double',
    'thinThickLargeGap': 'double',
    'thinThickMediumGap': 'double',
    'thinThickSmallGap': 'double',
    'thinThickThinLargeGap': 'double',
    'thinThickThinMediumGap': 'double',
    'thinThickThinSmallGap': 'double',
    'threeDEmboss': 'ridge',
    'threeDEngrave': 'groove',
    'triple': 'double',
}  # }}}

# Read from XML {{{

border_props = ('padding_%s', 'border_%s_width', 'border_%s_style', 'border_%s_color')
border_edges = ('left', 'top', 'right', 'bottom', 'between')


def read_single_border(parent, edge, XPath, get):
    color = style = width = padding = None
    for elem in XPath(f'./w:{edge}')(parent):
        c = get(elem, 'w:color')
        if c is not None:
            color = simple_color(c)
        s = get(elem, 'w:val')
        if s is not None:
            style = LINE_STYLES.get(s, 'solid')
        space = get(elem, 'w:space')
        if space is not None:
            try:
                padding = float(space)
            except ValueError, TypeError:
                pass
        sz = get(elem, 'w:sz')
        if sz is not None:
            # we don't care about art borders (they are only used for page borders)
            try:
                width = min(96, max(2, float(sz))) / 8
            except ValueError, TypeError:
                pass
    return dict(zip(border_props, (padding, width, style, color)))


def read_border(parent, dest, XPath, get, border_edges=border_edges, name='pBdr'):
    vals = {k % edge: inherit for edge in border_edges for k in border_props}

    for border in XPath('./w:' + name)(parent):
        for edge in border_edges:
            for prop, val in read_single_border(border, edge, XPath, get).items():
                if val is not None:
                    vals[prop % edge] = val

    for key, val in vals.items():
        setattr(dest, key, val)


def border_to_css(edge, style, css):
    bs = getattr(style, f'border_{edge}_style')
    bc = getattr(style, f'border_{edge}_color')
    bw = getattr(style, f'border_{edge}_width')
    if isinstance(bw, numbers.Number):
        # WebKit needs at least 1pt to render borders and 3pt to render double borders
        bw = max(bw, (3 if bs == 'double' else 1))
    if bs is not inherit and bs is not None:
        css[f'border-{edge}-style'] = bs
    if bc is not inherit and bc is not None:
        css[f'border-{edge}-color'] = bc
    if bw is not inherit and bw is not None:
        if isinstance(bw, numbers.Number):
            bw = f'{bw:.3g}pt'
        css[f'border-{edge}-width'] = bw


def read_indent(parent, dest, XPath, get):
    padding_left = padding_right = text_indent = inherit
    is_rtl = getattr(dest, 'bidi', False)
    for indent in XPath('./w:ind')(parent):
        start = get(indent, 'w:start')
        end = get(indent, 'w:end')
        startChars = get(indent, 'w:startChars')
        endChars = get(indent, 'w:endChars')
        left, leftChars, right, rightChars = start, startChars, end, endChars
        if is_rtl and is_rtl is not inherit:
            # TODO: For inherit we should be using textDirection from sectPr
            left, leftChars, right, rightChars = end, endChars, start, startChars
        l, lc = left or get(indent, 'w:left'), leftChars or get(indent, 'w:leftChars')
        pl = simple_float(lc, 0.01) if lc is not None else simple_float(l, 0.05) if l is not None else None
        if pl is not None:
            padding_left = '{:.3g}{}'.format(pl, 'em' if lc is not None else 'pt')

        r, rc = right or get(indent, 'w:right'), rightChars or get(indent, 'w:rightChars')
        pr = simple_float(rc, 0.01) if rc is not None else simple_float(r, 0.05) if r is not None else None
        if pr is not None:
            padding_right = '{:.3g}{}'.format(pr, 'em' if rc is not None else 'pt')

        h, hc = get(indent, 'w:hanging'), get(indent, 'w:hangingChars')
        fl, flc = get(indent, 'w:firstLine'), get(indent, 'w:firstLineChars')
        h = h if h is None else '-' + h
        hc = hc if hc is None else '-' + hc
        ti = (
            simple_float(hc, 0.01)
            if hc is not None
            else simple_float(h, 0.05)
            if h is not None
            else simple_float(flc, 0.01)
            if flc is not None
            else simple_float(fl, 0.05)
            if fl is not None
            else None
        )
        if ti is not None:
            text_indent = '{:.3g}{}'.format(ti, 'em' if hc is not None or (h is None and flc is not None) else 'pt')

    setattr(dest, 'margin_left', padding_left)
    setattr(dest, 'margin_right', padding_right)
    setattr(dest, 'text_indent', text_indent)


def read_justification(parent, dest, XPath, get):
    ans = inherit
    for jc in XPath('./w:jc[@w:val]')(parent):
        val = get(jc, 'w:val')
        if not val:
            continue
        if val in {'both', 'distribute'} or 'thai' in val or 'kashida' in val:
            ans = 'justify'
        elif val in {'left', 'center', 'right', 'start', 'end'}:
            ans = val
        elif val in {'start', 'end'}:
            ans = {'start': 'left'}.get(val, 'right')
    setattr(dest, 'text_align', ans)


def read_spacing(parent, dest, XPath, get):
    padding_top = padding_bottom = line_height = inherit
    for s in XPath('./w:spacing')(parent):
        a, al, aa = get(s, 'w:after'), get(s, 'w:afterLines'), get(s, 'w:afterAutospacing')
        pb = None if aa in {'on', '1', 'true'} else simple_float(al, 0.02) if al is not None else simple_float(a, 0.05) if a is not None else None
        if pb is not None:
            padding_bottom = '{:.3g}{}'.format(pb, 'ex' if al is not None else 'pt')

        b, bl, bb = get(s, 'w:before'), get(s, 'w:beforeLines'), get(s, 'w:beforeAutospacing')
        pt = None if bb in {'on', '1', 'true'} else simple_float(bl, 0.02) if bl is not None else simple_float(b, 0.05) if b is not None else None
        if pt is not None:
            padding_top = '{:.3g}{}'.format(pt, 'ex' if bl is not None else 'pt')

        l, lr = get(s, 'w:line'), get(s, 'w:lineRule', 'auto')
        if l is not None:
            lh = simple_float(l, 0.05) if lr in {'exact', 'atLeast'} else simple_float(l, 1 / 240.0)
            if lh is not None:
                line_height = '{:.3g}{}'.format(lh, 'pt' if lr in {'exact', 'atLeast'} else '')

    setattr(dest, 'margin_top', padding_top)
    setattr(dest, 'margin_bottom', padding_bottom)
    setattr(dest, 'line_height', line_height)


def read_shd(parent, dest, XPath, get):
    ans = inherit
    for shd in XPath('./w:shd[@w:fill]')(parent):
        val = get(shd, 'w:fill')
        if val:
            ans = simple_color(val, auto='transparent')
    setattr(dest, 'background_color', ans)


def read_numbering(parent, dest, XPath, get):
    lvl = num_id = inherit
    for np in XPath('./w:numPr')(parent):
        for ilvl in XPath('./w:ilvl[@w:val]')(np):
            try:
                lvl = int(get(ilvl, 'w:val'))
            except ValueError, TypeError:
                pass
        for num in XPath('./w:numId[@w:val]')(np):
            num_id = get(num, 'w:val')
    setattr(dest, 'numbering_id', num_id)
    setattr(dest, 'numbering_level', lvl)


class Frame:
    all_attributes = (
        'drop_cap',
        'h',
        'w',
        'h_anchor',
        'h_rule',
        'v_anchor',
        'wrap',
        'h_space',
        'v_space',
        'lines',
        'x_align',
        'y_align',
        'x',
        'y',
    )

    def __init__(self, fp, XPath, get):
        self.drop_cap = get(fp, 'w:dropCap', 'none')
        try:
            self.h = int(get(fp, 'w:h')) / 20
        except ValueError, TypeError:
            self.h = 0
        try:
            self.w = int(get(fp, 'w:w')) / 20
        except ValueError, TypeError:
            self.w = None
        try:
            self.x = int(get(fp, 'w:x')) / 20
        except ValueError, TypeError:
            self.x = 0
        try:
            self.y = int(get(fp, 'w:y')) / 20
        except ValueError, TypeError:
            self.y = 0

        self.h_anchor = get(fp, 'w:hAnchor', 'page')
        self.h_rule = get(fp, 'w:hRule', 'auto')
        self.v_anchor = get(fp, 'w:vAnchor', 'page')
        self.wrap = get(fp, 'w:wrap', 'around')
        self.x_align = get(fp, 'w:xAlign')
        self.y_align = get(fp, 'w:yAlign')

        try:
            self.h_space = int(get(fp, 'w:hSpace')) / 20
        except ValueError, TypeError:
            self.h_space = 0
        try:
            self.v_space = int(get(fp, 'w:vSpace')) / 20
        except ValueError, TypeError:
            self.v_space = 0
        try:
            self.lines = int(get(fp, 'w:lines'))
        except ValueError, TypeError:
            self.lines = 1

    def css(self, page):
        is_dropcap = self.drop_cap in {'drop', 'margin'}
        ans = {'overflow': 'hidden'}

        if is_dropcap:
            ans['float'] = 'left'
            ans['margin'] = '0'
            ans['padding-right'] = '0.2em'
        else:
            if self.h_rule != 'auto':
                t = 'min-height' if self.h_rule == 'atLeast' else 'height'
                ans[t] = f'{self.h:.3g}pt'
            if self.w is not None:
                ans['width'] = f'{self.w:.3g}pt'
            ans['padding-top'] = ans['padding-bottom'] = f'{self.v_space:.3g}pt'
            if self.wrap not in {None, 'none'}:
                ans['padding-left'] = ans['padding-right'] = f'{self.h_space:.3g}pt'
                if self.x_align is None:
                    fl = 'left' if self.x / page.width < 0.5 else 'right'
                else:
                    fl = 'right' if self.x_align == 'right' else 'left'
                ans['float'] = fl
        return ans

    def __eq__(self, other):
        for x in self.all_attributes:
            if getattr(other, x, inherit) != getattr(self, x):
                return False
        return True

    def __ne__(self, other):
        return not self.__eq__(other)


def read_frame(parent, dest, XPath, get):
    ans = inherit
    for fp in XPath('./w:framePr')(parent):
        ans = Frame(fp, XPath, get)
    setattr(dest, 'frame', ans)


# }}}


class ParagraphStyle:
    all_properties = (
        'adjustRightInd',
        'autoSpaceDE',
        'autoSpaceDN',
        'bidi',
        'contextualSpacing',
        'keepLines',
        'keepNext',
        'mirrorIndents',
        'pageBreakBefore',
        'snapToGrid',
        'suppressLineNumbers',
        'suppressOverlap',
        'topLinePunct',
        'widowControl',
        'wordWrap',
        # Border margins padding
        'border_left_width',
        'border_left_style',
        'border_left_color',
        'padding_left',
        'border_top_width',
        'border_top_style',
        'border_top_color',
        'padding_top',
        'border_right_width',
        'border_right_style',
        'border_right_color',
        'padding_right',
        'border_bottom_width',
        'border_bottom_style',
        'border_bottom_color',
        'padding_bottom',
        'border_between_width',
        'border_between_style',
        'border_between_color',
        'padding_between',
        'margin_left',
        'margin_top',
        'margin_right',
        'margin_bottom',
        # Misc.
        'text_indent',
        'text_align',
        'line_height',
        'background_color',
        'numbering_id',
        'numbering_level',
        'font_family',
        'font_size',
        'color',
        'frame',
        'cs_font_size',
        'cs_font_family',
    )
    adjustRightInd = autoSpaceDE = autoSpaceDN = bidi = inherit
    contextualSpacing = keepLines = keepNext = mirrorIndents = inherit
    pageBreakBefore = snapToGrid = suppressLineNumbers = inherit
    suppressOverlap = topLinePunct = widowControl = wordWrap = inherit
    border_left_width = border_left_style = border_left_color = padding_left = inherit
    border_top_width = border_top_style = border_top_color = padding_top = inherit
    border_right_width = border_right_style = border_right_color = padding_right = inherit
    border_bottom_width = border_bottom_style = border_bottom_color = padding_bottom = inherit
    border_between_width = border_between_style = border_between_color = padding_between = inherit
    margin_left = margin_top = margin_right = margin_bottom = inherit
    text_indent = text_align = line_height = background_color = inherit
    numbering_id = numbering_level = font_family = font_size = inherit
    color = frame = cs_font_size = cs_font_family = inherit

    linked_style = None
    style_name: str | None = None

    def __init__(self, namespace, pPr=None):
        self.namespace = namespace
        if pPr is None:
            for p in self.all_properties:
                setattr(self, p, inherit)
        else:
            for p in (
                'adjustRightInd',
                'autoSpaceDE',
                'autoSpaceDN',
                'bidi',
                'contextualSpacing',
                'keepLines',
                'keepNext',
                'mirrorIndents',
                'pageBreakBefore',
                'snapToGrid',
                'suppressLineNumbers',
                'suppressOverlap',
                'topLinePunct',
                'widowControl',
                'wordWrap',
            ):
                setattr(self, p, binary_property(pPr, p, namespace.XPath, namespace.get))

            for x in ('border', 'indent', 'justification', 'spacing', 'shd', 'numbering', 'frame'):
                f = read_funcs[x]
                f(pPr, self, namespace.XPath, namespace.get)

            for s in namespace.XPath('./w:pStyle[@w:val]')(pPr):
                self.linked_style = namespace.get(s, 'w:val')

            self.font_family = self.font_size = self.color = self.cs_font_size = self.cs_font_family = inherit

        self._css = None
        self._border_key = None

    def update(self, other):
        for prop in self.all_properties:
            nval = getattr(other, prop)
            if nval is not inherit:
                setattr(self, prop, nval)
        if other.linked_style is not None:
            self.linked_style = other.linked_style

    def resolve_based_on(self, parent):
        for p in self.all_properties:
            val = getattr(self, p)
            if val is inherit:
                setattr(self, p, getattr(parent, p))

    @property
    def css(self):
        if self._css is None:
            self._css = c = OrderedDict()
            if self.keepLines is True:
                c['page-break-inside'] = 'avoid'
            if self.pageBreakBefore is True:
                c['page-break-before'] = 'always'
            if self.keepNext is True:
                c['page-break-after'] = 'avoid'
            for edge in ('left', 'top', 'right', 'bottom'):
                border_to_css(edge, self, c)
                val = getattr(self, f'padding_{edge}')
                if val is not inherit:
                    c[f'padding-{edge}'] = f'{val:.3g}pt'
                val = getattr(self, f'margin_{edge}')
                if val is not inherit:
                    c[f'margin-{edge}'] = val

            if self.line_height not in {inherit, '1'}:
                c['line-height'] = self.line_height

            for x in ('text_indent', 'background_color', 'font_family', 'font_size', 'color'):
                val = getattr(self, x)
                if val is not inherit:
                    if x == 'font_size':
                        val = f'{val:.3g}pt'
                    c[x.replace('_', '-')] = val
            ta = self.text_align
            if ta is not inherit:
                if self.bidi is True:
                    ta = {'left': 'right', 'right': 'left'}.get(ta, ta)
                c['text-align'] = ta

        return self._css

    @property
    def border_key(self):
        if self._border_key is None:
            k = []
            for edge in border_edges:
                for prop in border_props:
                    prop = prop % edge
                    k.append(getattr(self, prop))
            self._border_key = tuple(k)
        return self._border_key

    def has_identical_borders(self, other_style):
        return self.border_key == getattr(other_style, 'border_key', None)

    def clear_borders(self):
        for edge in border_edges[:-1]:
            for prop in ('width', 'color', 'style'):
                setattr(self, f'border_{edge}_{prop}', inherit)

    def clone_border_styles(self):
        style = ParagraphStyle(self.namespace)
        for edge in border_edges[:-1]:
            for prop in ('width', 'color', 'style'):
                attr = f'border_{edge}_{prop}'
                setattr(style, attr, getattr(self, attr))
        return style

    def apply_between_border(self):
        for prop in ('width', 'color', 'style'):
            setattr(self, f'border_bottom_{prop}', getattr(self, f'border_between_{prop}'))

    def has_visible_border(self):
        for edge in border_edges[:-1]:
            bw, bs = getattr(self, f'border_{edge}_width'), getattr(self, f'border_{edge}_style')
            if bw is not inherit and bw and bs is not inherit and bs != 'none':
                return True
        return False


read_funcs = {k[5:]: v for k, v in globals().items() if k.startswith('read_')}
