# Copyright 2004-2021 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from __future__ import print_function
from builtins import chr

include "linebreak.pxi"

cdef class Glyph:

    def __cinit__(self):
        self.variation = 0
        self.delta_x_offset = 0

    def __repr__(self):
        if self.variation == 0:
            return "<Glyph {0!r} time={1}>".format(self.character, self.time)
        else:
            return "<Glyph {0!r} vs={1} time={2}>".format(self.character, self.variation, self.time)

cdef class Line:

    def __init__(self, int y, int height, list glyphs):
        self.y = y
        self.height = height
        self.glyphs = glyphs
        self.eop = False

    def __repr__(self):
        return "<Line y={0}, height={1}>".format(self.y, self.height)

# The maximum width of text we lay out. This should be quite a bit smaller
# than the maximum SDL surface width. (16384)
cdef int MAX_WIDTH
MAX_WIDTH = 8192

TEXT=1
TAG=2
PARAGRAPH=3
DISPLAYABLE=4

def tokenize(unicode s):
    """
    This tokenizes a unicode string into text tags and tokens. It returns a list
    of pairs, where each pair begins with TEXT, TAG or PARAGRAPH, and then has
    the contents of the text run or tag.
    """

    cdef int TEXT_STATE = 1
    cdef int LEFT_BRACE_STATE = 2
    cdef int TAG_STATE = 3
    cdef int state = TEXT_STATE

    cdef Py_UNICODE c
    cdef unicode buf = u''

    cdef list rv = [ ]

    for c in s:

        if state == TEXT_STATE:

            if c == u'\n':
                if buf:
                    rv.append((TEXT, buf))

                rv.append((PARAGRAPH, u''))
                buf = u''

                continue

            elif c == u'{':
                state = LEFT_BRACE_STATE
                continue

            else:
                buf += c
                continue

        elif state == LEFT_BRACE_STATE:
            if c == u'{':
                buf += c
                state = TEXT_STATE
                continue

            elif c == u'}':
                raise Exception("Empty text tag in {0!r}.".format(s))

            else:
                if buf:
                    rv.append((TEXT, buf))

                buf = c
                state = TAG_STATE
                continue

        elif state == TAG_STATE:

            if c == u'}':
                rv.append((TAG, buf))
                buf = u''
                state = TEXT_STATE
                continue

            else:
                buf += c

    if state != TEXT_STATE:
        raise Exception("Open text tag at end of string {0!r}.".format(s))

    if buf:
        rv.append((TEXT, buf))

    return rv


def annotate_western(list glyphs):
    """
    Annotate the characters with line splitting information.
    """

    cdef Glyph g

    for g in glyphs:

        # Don't split ruby.
        if g.ruby != RUBY_NONE:
            continue

        if g.character == 0:
            g.split = SPLIT_BEFORE
        elif g.character == 0x20 or g.character == 0x200b:
            g.split = SPLIT_INSTEAD
        else:
            g.split = SPLIT_NONE

# This is used to tailor the unicode break algorithm. If a character in this
# array is mapped to not
cdef char break_tailor[65536]

for i in range(0, 65536):
    break_tailor[i] = BC_XX

def language_tailor(chars, cls):
    """
    :doc: other
    :args: (chars, cls)

    This can be used to override the line breaking class of a character. For
    example, the linebreaking class of a character can be set to ID to
    treat it as an ideograph, which allows breaks before and after that
    character.

    `chars`
        A string containing each of the characters to tailor.

    `cls`
        A string giving a character class. This should be one of the classes defined in Table
        1 of `UAX #14: Unicode Line Breaking Algorithm <http://www.unicode.org/reports/tr14/tr14-30.html>`_.
    """

    ncls = CLASSES.get(cls, BC_XX)

    for c in chars:
        break_tailor[ord(c)] = ncls

# cjk
#
# 0 = western
# 1 = loose
# 2 = normal
# 3 = strict
def annotate_unicode(list glyphs, bint no_ideographs, int cjk):
    """
    Annotate unicode characters with information as to if they can be used
    for linebreaking.
    """

    cdef char old_type, new_type, tailor_type
    cdef int space_pos, pos
    cdef int len_glyphs = len(glyphs)
    cdef int c
    cdef char bc
    cdef Glyph g, g1, old_g
    cdef char *break_classes

    old_type = BC_WJ
    pos = 1
    space_pos = 0

    if not glyphs:
        return

    if cjk == 0:
        break_classes = break_western
    elif cjk == 1:
        break_classes = break_cjk_loose
    elif cjk == 2:
        break_classes = break_cjk_normal
    elif cjk == 3:
        break_classes = break_cjk_strict
    else:
        break_classes = break_western

    for pos from 1 <= pos < len_glyphs:

        g = glyphs[pos]
        c = g.character

        if 0x20000 <= c <= 0x2ffff: # Supplemental Ideographic Plane
            new_type = BC_ID
            tailor_type = BC_XX
        elif c > 65535: # Other non-basic planes.
            new_type = BC_AL
            tailor_type = BC_XX
        else: # Basic plane - use lookup table.
            new_type = break_classes[c]
            tailor_type = break_tailor[c]

        # If given no-ideographs, then turn ideographs and hangul syllables
        # into alphabetic characters.
        if no_ideographs and (
            new_type == BC_H2 or
            new_type == BC_H3 or
            new_type == BC_ID or
            new_type == BC_JL or
            new_type == BC_JV or
            new_type == BC_JT):

            new_type = BC_AL

        # Normalize the class by turning various groups into AL.
        if (new_type >= BC_PITCH and new_type != BC_SP and new_type != BC_CB):
            new_type = BC_AL

        if tailor_type != BC_XX:
            new_type = tailor_type

        # If we have a space, record it and continue.
        if new_type == BC_SP:
            g.split = SPLIT_NONE
            space_pos = pos
            continue

        # If we have a combining mark, continue.
        if new_type == BC_CM:
            g.split = SPLIT_NONE
            continue

        if new_type == BC_CB:
            if old_type == BC_WJ or old_type == BC_GL:
                g.split = SPLIT_NONE
            else:
                g.split = SPLIT_BEFORE

            continue

        if old_type == BC_CB:
            if new_type == BC_WJ or new_type == BC_GL:
                g.split = SPLIT_NONE
            else:
                g.split = SPLIT_BEFORE

            continue

        # Figure out the type of break opportunity we have here.
        # ^ Prohibited break.
        # % Indirect break.
        # @ Prohibited break (combining mark)
        # # Indirect break (combining mark)
        # _ Direct break.

        bc = break_rules[ old_type * BC_PITCH + new_type]

        if bc == "%": # Indirect break.
            if space_pos:
                g1 = glyphs[space_pos]
                g1.split = SPLIT_INSTEAD

            g.split = SPLIT_NONE

        elif bc == "_": # Direct break.
            if space_pos:
                g1 = glyphs[space_pos]
                g1.split = SPLIT_INSTEAD
                g.split = SPLIT_NONE
            else:
                g.split = SPLIT_BEFORE

        else:
            g.split = SPLIT_NONE

        old_type = new_type
        space_pos = 0

    # Deal with ruby, by marking it as non-spacing.
    old_g = glyphs[0]
    old_g.split = SPLIT_NONE

    for g in glyphs:

        if g.character == 0:
            g.split = SPLIT_BEFORE

        if g.ruby == RUBY_TOP or g.ruby == RUBY_ALT:
            g.split = SPLIT_NONE

        elif g.ruby == RUBY_BOTTOM and old_g.ruby == RUBY_BOTTOM:
            g.split = SPLIT_NONE

        old_g = g



def linebreak_greedy(list glyphs, int first_width, int rest_width):
    """
    Starting with a list of glyphs, decides where to split it. The result of
    this is the same list of glyphs, but with some of the .split fields set
    back to SPLIT_NONE, where we decided not to go ahead with the split after
    all.

    This is the greedy algorithm, which splits when the line is longer
    than a specified width.
    """

    cdef Glyph g, split_g
    cdef float width, x, splitx, gwidth

    width = first_width
    split_g = None

    # The x position of the current character. Invariant: x can never be more
    # that one character-width greater than width.
    x = 0

    # The x position after splitting the line.
    splitx = 0

    for g in glyphs:

        if g.ruby == RUBY_TOP:
            continue

        if g.ruby == RUBY_ALT:
            continue

        # If the x coordinate is greater than the width of the screen,
        # split at the last split point, if any.
        if x > width and split_g is not None:
            x = splitx
            split_g = None
            width = rest_width

        x += g.advance
        splitx += g.advance

        if g.split == SPLIT_INSTEAD:
            if split_g is not None:
                split_g.split = SPLIT_NONE

            split_g = g
            splitx = 0

        elif g.split == SPLIT_BEFORE:

            if split_g is not None:
                split_g.split = SPLIT_NONE

            split_g = g
            splitx = g.advance

    # Split at the last character, if necessary.
    if x > width:
        split_g = None

    if split_g is not None:
        split_g.split = SPLIT_NONE

def linebreak_nobreak(list glyphs):
    """
    Linebreak without linebreaking.
    """

    cdef Glyph g

    for g in glyphs:
        g.split = SPLIT_NONE


def linebreak_debug(list glyphs):
    """
    Return a string giving the results of linebreaking a list of glyphs.
    """

    cdef Glyph g

    rv = ""

    for g in glyphs:

        if g.split == SPLIT_INSTEAD:
            rv += "|"
        elif g.split == SPLIT_BEFORE:
            rv += "[" + chr(g.character)
        else:
            rv += chr(g.character)

    return rv


def linebreak_list(list glyphs):
    """
    Returns a list of unicode strings, one per broken line.
    """

    cdef Glyph g

    rv = [ ]
    line = u""

    for g in glyphs:

        if g.split == SPLIT_INSTEAD:
            rv.append(line)
            line = u""
        elif g.split == SPLIT_BEFORE:
            rv.append(line)
            line = chr(g.character)
        else:
            line += chr(g.character)

    if line:
        rv.append(line)

    return rv


def place_horizontal(list glyphs, float start_x, float first_indent, float rest_indent):
    """
    Place the glyphs horizontally, without taking into account the indentation
    at the start of the line. Returns the width of the laid-out line.
    """

    if not glyphs:
        return 0

    cdef Glyph g, old_g
    cdef float x, maxx

    x = start_x + first_indent
    maxx = 0
    old_g = None

    for g in glyphs:

        if g.ruby == RUBY_TOP:
            continue

        if g.ruby == RUBY_ALT:
            continue

        if g.split != SPLIT_NONE and old_g:
            # When a glyph is at the end of the line, set its advance to
            # be its width. (This makes things like strikeout and underline
            # easier, since we only need consider advance.)
            old_g.advance = old_g.width

        if g.split == SPLIT_INSTEAD:
            x = start_x + rest_indent
            continue

        elif g.split == SPLIT_BEFORE:
            x = start_x + rest_indent

        g.x = <short> (x + .5)

        if maxx < x + g.width:
            maxx = x + g.width
        if maxx < x + g.advance:
            maxx = x + g.advance

        x += g.advance

        # Limit us to some width.
        if x > MAX_WIDTH:
            x = MAX_WIDTH

        old_g = g

    return maxx

def place_vertical(list glyphs, int y, int spacing, int leading):
    """
    Vertically places the non-ruby glyphs. Returns a list of line end heights,
    and the y-value for the top of the next line.
    """

    cdef Glyph g, gg

    cdef int pos, sol, len_glyphs, i
    cdef int ascent, line_spacing
    cdef bint end_line

    if not glyphs:
        return [ ], y

    len_glyphs = len(glyphs)

    pos = 0
    sol = 0

    ascent = 0
    line_spacing = 0

    rv = [ ]

    y += leading

    while True:

        if pos >= len_glyphs:
            end_line = True
        else:
            g = glyphs[pos]
            end_line = (g.split != SPLIT_NONE)

        if end_line:

            for i from sol <= i < pos:
                gg = glyphs[i]

                if gg.ruby == RUBY_TOP:
                    continue

                if gg.ruby == RUBY_ALT:
                    continue

                if gg.ascent:
                    gg.y = y + ascent

                else:
                    # Glyphs without ascents are displayables, which get
                    # aligned to the top of the line.
                    gg.y = y
                    gg.ascent = ascent

            l = Line(y - leading, leading + line_spacing + spacing, glyphs[sol:pos])
            rv.append(l)

            y += line_spacing
            y += spacing
            y += leading

            sol = pos

            ascent = 0
            line_spacing = 0

            if g.split == SPLIT_INSTEAD:
                sol += 1
                pos += 1

                if pos >= len_glyphs:
                    break
                else:
                    continue

        if pos >= len_glyphs:
            break

        if g.ascent > ascent:
            ascent = g.ascent

        if g.line_spacing > line_spacing:
            line_spacing = g.line_spacing

        pos += 1


    rv[-1].eop = True
    return rv, y - leading

def kerning(list glyphs, float amount):
    cdef Glyph g

    for g in glyphs:
        g.advance += amount


def assign_times(float t, float gps, list glyphs):
    """
    Assign a display time to each glyph.

    `t`
        The start time of the first glyph.

    `gps`
        The number of glyphs per second to show.

    `glyphs`
        A list of glyphs to apply this to.

    Returns the time of the first glyph in the next block.
    """

    cdef float tpg # time per glyph
    cdef Glyph g

    if gps == 0:
        tpg = 0.0
    else:
        tpg = 1.0 / gps

    for g in glyphs:

        if (g.ruby == RUBY_TOP) or (g.ruby == RUBY_ALT):
            g.time = -1
            continue

        t += tpg
        g.time = t


    return t


def max_times(list l):
    """
    Set the max_time filed on each line.
    """

    cdef Line line
    cdef Glyph g
    cdef float max_time

    max_time = 0

    for line in l:
        for g in line.glyphs:
            if g.time > max_time:
                max_time = g.time

        line.max_time = max_time

    return max_time


def hyperlink_areas(list l):
    """
    Returns a list of (hyperlink, x, y, w, h) tuples, where each entry in
    the rectangle represents a contiguous portion of a hyperlink on the
    given line.
    """

    cdef Line line
    cdef Glyph g
    cdef list gl
    cdef int len_gl

    cdef int pos

    cdef int max_x
    cdef int min_x
    cdef int hyperlink

    rv = [ ]

    for line in l:
        gl = line.glyphs
        len_gl = len(gl)

        hyperlink = 0
        max_x = 0
        min_x = 1000000
        pos = 0

        while pos < len_gl:

            g = gl[pos]

            if (hyperlink and g.hyperlink != hyperlink):
                rv.append((hyperlink, min_x, line.y, max_x - min_x, line.height))
                hyperlink = 0
                max_x = 0
                min_x = 1000000

            hyperlink = g.hyperlink

            if hyperlink:
                if g.x < min_x:
                    min_x = g.x

                if g.x + g.width > max_x:
                    max_x = g.x + <int> g.width

            pos += 1

        if hyperlink:
            rv.append((hyperlink, min_x, line.y, max_x - min_x, line.height))

    return rv


def mark_ruby_top(list l):

    cdef Glyph g

    for g in l:
        g.ruby = RUBY_TOP

def mark_altruby_top(list l):

    cdef Glyph g

    for g in l:
        g.ruby = RUBY_ALT


def mark_ruby_bottom(list l):

    cdef Glyph g

    for g in l:
        g.ruby = RUBY_BOTTOM


def place_ruby(list glyphs, int ruby_offset, int altruby_offset, int surf_width, int surf_height):

    cdef Glyph g
    cdef ruby_t last_ruby = RUBY_NONE
    cdef int len_glyphs = len(glyphs)
    cdef float x, width, min_x = 0, max_x = 0
    cdef int y = 0
    cdef int start_top
    cdef int pos = 0
    cdef int i

    while pos < len_glyphs:

        g = glyphs[pos]

        if g.ruby == RUBY_NONE:

            min_x = g.x
            max_x = g.x + g.width
            y = g.y

            last_ruby = RUBY_NONE

            pos += 1
            continue

        elif g.ruby == RUBY_BOTTOM:

            if last_ruby != RUBY_BOTTOM:
                min_x = g.x

            max_x = g.x + g.width
            y = g.y

            last_ruby = RUBY_BOTTOM

            pos += 1
            continue

        # Otherwise, we have RUBY_TOP or RUBY_ALT

        # Find the run of RUBY_TOP. When this is done, the run will be in
        # glyphs[start_top:pos].
        start_top = pos

        last_ruby = g.ruby

        while pos < len_glyphs:

            g = glyphs[pos]
            if g.ruby != last_ruby:
                break

            pos += 1

        # Compute the width of the run.

        width = 0
        for i from start_top <= i < pos:
            g = glyphs[i]
            width += g.advance

        width -= glyphs[pos - 1].advance
        width += glyphs[pos - 1].width

        # Place the glyphs.
        x = (max_x + min_x) / 2 - width / 2

        for i from start_top <= i < pos:
            g = glyphs[i]
            g.x = <int> (x + .5)

            if g.ruby == RUBY_TOP:
                g.y = y + ruby_offset
            else:
                g.y = y + altruby_offset

            x += g.advance


def align_and_justify(list lines, int width, float text_align, bint justify):
    """
    Handle text alignment and justification.
    """

    cdef Line l
    cdef Glyph g

    cdef int max_x
    cdef int spaces

    cdef float justify_offset
    cdef float justify_per_space

    cdef int offset

    # See if we have to do anything at all.
    if not justify and text_align == 0.0:
        return

    for l in lines:

        spaces = 0
        max_x = 0

        for g in l.glyphs:

            if g.ruby == RUBY_TOP:
                continue

            if g.ruby == RUBY_ALT:
                continue

            if g.character == 0x20:
                spaces += 1

            max_x = <int> (g.x + g.width)

        # If we're too big, give up.
        if max_x >= MAX_WIDTH:
            continue

        if justify and spaces and not l.eop:

            justify_per_space = 1.0 * (width - max_x) / spaces
            justify_offset = 0.5 # Makes numbers round better.

            for g in l.glyphs:

                if g.ruby == RUBY_TOP:
                    continue

                if g.ruby == RUBY_ALT:
                    continue

                if g.character == 0x20:
                    justify_offset += justify_per_space

                g.x += <int> justify_offset

        else:
            offset = <int> ((width - max_x) * text_align)

            for g in l.glyphs:

                if g.ruby == RUBY_TOP:
                    continue

                if g.ruby == RUBY_ALT:
                    continue

                g.x += offset

def reverse_lines(list glyphs):
    """
    Reverses each line in glyphs, while keeping the lines themselves in
    the original order.
    """

    cdef list rv
    cdef list block
    cdef Glyph g

    rv = [ ]
    block = [ ]

    for g in glyphs:

        if g.split == SPLIT_INSTEAD:
            block.reverse()
            rv.extend(block)
            rv.append(g)
            block = [ ]

            continue

        block.append(g)

    block.reverse()
    rv.extend(block)

    return rv

def copy_splits(list source, list dest):
    """
    Copies break and timing information from one list of glyphs
    to another.
    """

    cdef Glyph s
    cdef Glyph d
    cdef int i

    for 0 <= i < len(dest):
        s = source[i]
        d = dest[i]

        d.split = s.split


def tweak_glyph_spacing(list glyphs, list lines, double dx, double dy, double w, double h):
    cdef Glyph g

    if w <= 0 or h <= 0:
        return

    cdef short old_x_offset = 0
    cdef short x_offset

    for g in glyphs:

        x_offset = <short> (dx * g.x / w)

        g.x += x_offset
        g.y += <short> (dy * g.y / h)

        if x_offset > old_x_offset:
            g.delta_x_offset = x_offset - old_x_offset

        old_x_offset = x_offset

    for l in lines:
        end = l.y + l.height

        l.y += int(dy * l.y / h)
        end += int(dy * end / h)

        l.height = end - l.y

def offset_glyphs(list glyphs, short x, short y):
    cdef Glyph g

    if x == 0 and y == 0:
        return

    for g in glyphs:
        g.x += x
        g.y += y


