#
# font_asst.py -- Font assistant routines
#
# This is open-source software licensed under a BSD license.
# Please see the file LICENSE.txt for details.
#
import os
import re
from collections import namedtuple

from ginga.misc import Bunch

# describe a font record
Font = namedtuple('Font', ['family', 'style', 'weight'])

# lookup table of cached fonts, for backends that build them
font_cache = dict()

# lookup table of font
font_dir = dict()

# Set up font alias mapping
aliases = dict()

# font scaling factors
default_scaling_factor = 1.0
scaling_factor = Bunch.Bunch(caseless=True)

font_regex = re.compile(r'^([\w\s\d\-\_;]+)$')
default_family = 'sans'

# generic CSS font-family keywords -- these are left unquoted in a CSS
# font-family list so the CSS engine (browser or Pango/GTK) treats them
# as generic fallbacks rather than literal family names
css_generic_families = set(['serif', 'sans-serif', 'monospace',
                            'cursive', 'fantasy', 'system-ui'])


def add_substitutes(family, family_lst):
    global aliases
    family = family.lower()
    lst = aliases.setdefault(family, [])
    for sub_family in family_lst:
        if sub_family not in lst:
            lst.append(sub_family)


def add_substitute(family, sub_family):
    return add_substitutes(family, [sub_family])


def add_alias(family, alt_family):
    global aliases
    family = family.lower()
    lst = aliases.setdefault(family, [])
    # insert as the preferential font
    lst.insert(0, alt_family)


def get_substitutes(family):
    """Get known substitutes for a font family.
    e.g. 'fixed' => ['ubuntu mono', 'monospace', 'Courier']
    """
    global aliases
    res = aliases.get(family.lower(), [default_family])
    return res


def resolve_alias(family, subst_name):
    # If the family has a registered alias/substitute, use the preferred one.
    # Otherwise keep ``subst_name`` (the caller's original) rather than
    # collapsing to the default family -- ``family`` may be a full
    # 'family;style;weight' spec, and dropping it to the default loses the
    # requested family, style and weight.
    lst = aliases.get(family.lower())
    if lst is None:
        return subst_name
    return lst[0]


def get_css_family_list(family):
    """Return a CSS ``font-family`` value for `family`.

    For backends that style a widget's font through CSS (the web/pg
    backend and the GTK ``CssProvider`` path), a single family name like
    'fixed' or 'sans' can't be resolved -- the CSS engine matches it
    literally and silently falls back to a default.  Instead, expand the
    family into a comma-separated fallback list built from its registered
    substitutes (see `get_substitutes`), so the CSS engine can fall back
    gracefully when the preferred face isn't available.  Generic keywords
    (monospace/sans-serif/...) are emitted unquoted; everything else is
    quoted so multi-word family names survive.
    """
    families = aliases.get(family.lower())
    if not families:
        families = [family]
    out, seen = [], set()
    for fam in families:
        key = fam.lower()
        if key in seen:
            continue
        seen.add(key)
        out.append(fam if key in css_generic_families else '"{}"'.format(fam))
    return ", ".join(out)


def get_cache(font_key):
    """Return a previously cached font object appropriate to the backend
    based on a key generated by the backend.
    Will return a KeyError in the case of a missing font.
    """
    font = font_cache[font_key]
    return font


def add_cache(font_key, font):
    """Add a `font` object to the cache, placed under key `font_key`.
    """
    global font_cache
    font_cache[font_key] = font


def add_loadable_font(font_file, family, style='normal', weight='normal'):
    """Add a font description to our directory of externally loadable fonts.
    `font_file` is the path to the font, and `family` is the name to register
    it under.
    """
    global font_dir

    style = style.lower()
    weight = weight.lower()

    font_tup = Font(family=family, style=style, weight=weight)
    font_dir[font_tup] = Bunch.Bunch(name=family, font_path=font_file,
                                     style=style, weight=weight)
    # family is always a substitute for itself
    add_substitute(family, family)
    return font_tup


def have_loadable_font(font_tup):
    """Return True if the given font tuple is registered as one of our
    externally loadable fonts.
    """
    return font_tup in font_dir


def get_font_info(font_tup):
    """Return known info on an externally loadable font (including its path).
    """
    try:
        return font_dir[font_tup]
    except KeyError:
        raise KeyError(str(font_tup))


def remove_font(font_tup):
    """Remove `font_name` from the directory of loadable fonts.
    """
    global font_dir
    try:
        del font_dir[font_tup]
    except KeyError:
        pass


def get_loadable_fonts():
    """Return the sequence of externally loadable fonts.
    """
    return list(font_dir.keys())


def get_font_attrs(font_str):
    vals = font_str.lower().split(';')
    style, weight = 'normal', 'normal'
    family = vals[0].strip().replace('"', '').replace("'", '')
    if len(vals) > 1:
        style = vals[1].strip()
    if len(vals) > 2:
        weight = vals[2].strip()
    return (family, style, weight)


def parse_font(font_str):
    """Extract font information from a font string, such as supplied to the
    'font' argument to a widget, and return a Font named tuple.
    """
    match = font_regex.match(font_str)
    if not match:
        raise ValueError(f"font spec '{font_str}' doesn't match expected format")

    family, style, weight = get_font_attrs(font_str)
    return Font(family=family, style=style, weight=weight)


def scale_fontsize(key, fontsize):
    factor = scaling_factor.get(key, default_scaling_factor)
    return int(round(fontsize * factor))


def set_scale_factor(key, factor):
    """Set the scale factor for a renderer `key` to `factor`.
    """
    global scaling_factor
    scaling_factor[key] = factor


def calc_font_size(win_wd):
    """Heuristic to calculate an appropriate font size based on the
    width of the viewer window.

    Parameters
    ----------
    win_wd : int
        The width of the viewer window.

    Returns
    -------
    font_size : int
        Approximately appropriate font size in points

    """
    font_size = 4
    if win_wd >= 1600:
        font_size = 24
    elif win_wd >= 1000:
        font_size = 18
    elif win_wd >= 800:
        font_size = 16
    elif win_wd >= 600:
        font_size = 14
    elif win_wd >= 500:
        font_size = 12
    elif win_wd >= 400:
        font_size = 11
    elif win_wd >= 300:
        font_size = 10
    elif win_wd >= 250:
        font_size = 8
    elif win_wd >= 200:
        font_size = 6

    return font_size


# --- Set up bundled fonts ---

fontdir, xx = os.path.split(__file__)

add_loadable_font(os.path.join(fontdir, 'Roboto', "Roboto-Regular.ttf"), 'roboto')
for weight in ["Black", "Bold", "Light", "Medium", "Thin"]:
    lweight = weight.lower()
    add_loadable_font(os.path.join(fontdir, 'Roboto', f"Roboto-{weight}.ttf"),
                      'roboto', weight=lweight)

add_loadable_font(os.path.join(fontdir, 'Roboto_Condensed', "RobotoCondensed-Regular.ttf"),
                  'roboto condensed')
for weight in ["Bold", "BoldItalic", "Italic", "Light", "LightItalic"]:
    lweight = weight.lower()
    style = 'normal'
    if 'italic' in lweight:
        lweight = lweight.replace('italic', '')
        if len(lweight) == 0:
            lweight = 'normal'
        style = 'italic'
    add_loadable_font(os.path.join(fontdir, 'Roboto_Condensed', f"RobotoCondensed-{weight}.ttf"),
                      'roboto condensed', style=style, weight=lweight)

add_loadable_font(os.path.join(fontdir, 'Ubuntu_Mono', "UbuntuMono-Regular.ttf"),
                  'ubuntu mono', style='normal', weight='normal')
for weight in ["Bold", "BoldItalic", "Italic"]:
    lweight = weight.lower()
    style = 'normal'
    if 'italic' in lweight:
        lweight = lweight.replace('italic', '')
        if len(lweight) == 0:
            lweight = 'normal'
        style = 'italic'
    add_loadable_font(os.path.join(fontdir, 'Ubuntu_Mono', f"UbuntuMono-{weight}.ttf"),
                      'ubuntu mono', style=style, weight=lweight)

# house font needs to be available
add_substitutes('house', ['ubuntu mono', 'monospace', 'courier new', 'courier'])
add_substitutes('fixed', ['ubuntu mono', 'monospace', 'courier new', 'courier'])
add_substitutes('monospace', ['ubuntu mono', 'monospace', 'courier new', 'courier'])
add_substitutes('sans', ['roboto', 'arial', 'helvetica'])
add_substitutes('sans serif', ['roboto', 'arial', 'helvetica'])
add_substitutes('sans-serif', ['roboto', 'arial', 'helvetica'])
add_substitutes('sans condensed', ['roboto condensed'])
add_substitutes('sans-serif condensed', ['roboto condensed'])
