#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>

import ctypes
import sys
import unicodedata
from collections import namedtuple
from functools import lru_cache
from itertools import chain
from threading import Lock

from kitty.fast_data_types import FT_PIXEL_MODE_GRAY, Face
from kitty.fonts.box_drawing import render_missing_glyph
from kitty.utils import ceil_int, get_logical_dpi, safe_print, wcwidth

from .fontconfig import (
    FontNotFound, find_font_for_character, font_for_family, get_font_files
)

current_font_family = current_font_family_name = cff_size = cell_width = cell_height = baseline = None
CharTexture = underline_position = underline_thickness = None
alt_face_cache = {}
symbol_map = {}


def set_char_size(face, width=0, height=0, hres=0, vres=0):
    face.set_char_size(width, height, hres, vres)


def load_char(font, face, text):
    face.load_char(text, font.hinting, font.hintstyle)


def calc_cell_width(font, face):
    ans = 0
    for i in range(32, 128):
        ch = chr(i)
        load_char(font, face, ch)
        m = face.glyph_metrics()
        ans = max(ans, ceil_int(m.horiAdvance / 64))
    return ans


@lru_cache(maxsize=2**10)
def font_for_char(char, bold=False, italic=False, allow_bitmaped_fonts=False):
    if allow_bitmaped_fonts:
        return find_font_for_character(
            current_font_family_name,
            char,
            bold,
            italic,
            allow_bitmaped_fonts=True,
            size_in_pts=cff_size['width'] / 64,
            dpi=(cff_size['hres'] + cff_size['vres']) / 2
        )
    return find_font_for_character(
        current_font_family_name, char, bold, italic
    )


def install_symbol_map(val):
    global symbol_map
    symbol_map = {}
    family_map = {f: font_for_family(f) for f in set(val.values())}
    for ch, family in val.items():
        symbol_map[ch] = family_map[family]


def font_units_to_pixels(x, units_per_em, size_in_pts, dpi):
    return ceil_int(x * ((size_in_pts * dpi) / (72 * units_per_em)))


def set_font_family(opts, override_font_size=None):
    global current_font_family, current_font_family_name, cff_size, cell_width, cell_height, CharTexture, baseline
    global underline_position, underline_thickness
    size_in_pts = override_font_size or opts.font_size
    current_font_family = get_font_files(opts)
    current_font_family_name = opts.font_family
    dpi = get_logical_dpi()
    cff_size = ceil_int(64 * size_in_pts)
    cff_size = {
        'width': cff_size,
        'height': cff_size,
        'hres': int(dpi[0]),
        'vres': int(dpi[1])
    }
    install_symbol_map(opts.symbol_map)
    for fobj in chain(current_font_family.values(), symbol_map.values()):
        set_char_size(fobj.face, **cff_size)
    face = current_font_family['regular'].face
    cell_width = calc_cell_width(current_font_family['regular'], face)
    cell_height = font_units_to_pixels(
        face.height, face.units_per_EM, size_in_pts, dpi[1]
    )
    baseline = font_units_to_pixels(
        face.ascender, face.units_per_EM, size_in_pts, dpi[1]
    )
    underline_position = min(
        baseline - font_units_to_pixels(
            face.underline_position, face.units_per_EM, size_in_pts, dpi[1]
        ), cell_height - 1
    )
    underline_thickness = font_units_to_pixels(
        face.underline_thickness, face.units_per_EM, size_in_pts, dpi[1]
    )
    CharTexture = ctypes.c_ubyte * (cell_width * cell_height)
    font_for_char.cache_clear()
    alt_face_cache.clear()
    return cell_width, cell_height


CharBitmap = namedtuple(
    'CharBitmap', 'data bearingX bearingY advance rows columns'
)
freetype_lock = Lock()


def render_to_bitmap(font, face, text):
    load_char(font, face, text)
    bitmap = face.bitmap()
    if bitmap.pixel_mode != FT_PIXEL_MODE_GRAY:
        raise ValueError(
            'FreeType rendered the glyph for {!r} with an unsupported pixel mode: {}'.
            format(text, bitmap.pixel_mode)
        )
    return bitmap


def render_using_face(font, face, text, width, italic, bold):
    bitmap = render_to_bitmap(font, face, text)
    if width == 1 and bitmap.width > cell_width:
        extra = bitmap.width - cell_width
        if italic and extra < cell_width // 2:
            bitmap = face.trim_to_width(bitmap, cell_width)
        elif extra > max(2, 0.3 * cell_width) and face.is_scalable:
            # rescale the font size so that the glyph is visible in a single
            # cell and hope somebody updates libc's wcwidth
            sz = cff_size.copy()
            sz['width'] = int(sz['width'] * cell_width / bitmap.width)
            # Preserve aspect ratio
            sz['height'] = int(sz['height'] * cell_width / bitmap.width)
            try:
                set_char_size(face, **sz)
                bitmap = render_to_bitmap(font, face, text)
            finally:
                set_char_size(face, **cff_size)
    m = face.glyph_metrics()
    return CharBitmap(
        bitmap.buffer,
        ceil_int(abs(m.horiBearingX) / 64),
        ceil_int(abs(m.horiBearingY) / 64),
        ceil_int(m.horiAdvance / 64), bitmap.rows, bitmap.width
    )


def render_char(text, bold=False, italic=False, width=1):
    key = 'regular'
    if bold:
        key = 'bi' if italic else 'bold'
    elif italic:
        key = 'italic'
    ch = text[0]
    with freetype_lock:
        font = symbol_map.get(ch)
        if font is None or not font.face.get_char_index(ch):
            font = current_font_family.get(key) or current_font_family['regular']
            face = font.face
            if not face.get_char_index(ch):
                try:
                    font = font_for_char(ch, bold, italic)
                except FontNotFound:
                    font = font_for_char(
                        ch, bold, italic, allow_bitmaped_fonts=True
                    )
                face = alt_face_cache.get(font)
                if face is None:
                    face = alt_face_cache[font] = Face(font.face, font.index)
                    if face.is_scalable:
                        set_char_size(face, **cff_size)
        else:
            face = font.face
        return render_using_face(font, face, text, width, italic, bold)


def place_char_in_cell(bitmap_char):
    # We want the glyph to be positioned inside the cell based on the bearingX
    # and bearingY values, making sure that it does not overflow the cell.

    # Calculate column bounds
    if bitmap_char.columns > cell_width:
        src_start_column, dest_start_column = 0, 0
    else:
        src_start_column, dest_start_column = 0, bitmap_char.bearingX
        extra = dest_start_column + bitmap_char.columns - cell_width
        if extra > 0:
            dest_start_column -= extra
    column_count = min(
        bitmap_char.columns - src_start_column, cell_width - dest_start_column
    )

    # Calculate row bounds, making sure the baseline is aligned with the cell
    # baseline
    if bitmap_char.bearingY > baseline:
        src_start_row, dest_start_row = bitmap_char.bearingY - baseline, 0
    else:
        src_start_row, dest_start_row = 0, baseline - bitmap_char.bearingY
    row_count = min(
        bitmap_char.rows - src_start_row, cell_height - dest_start_row
    )
    return create_cell_buffer(
        bitmap_char, src_start_row, dest_start_row, row_count,
        src_start_column, dest_start_column, column_count
    )


def split_char_bitmap(bitmap_char):
    stride = bitmap_char.columns
    extra = stride - cell_width
    rows = bitmap_char.rows
    first_buf = (ctypes.c_ubyte * (cell_width * rows))()
    second_buf = (ctypes.c_ubyte * (extra * rows))()
    src = bitmap_char.data
    for r in range(rows):
        soff, off = r * stride, r * cell_width
        first_buf[off:off + cell_width] = src[soff:soff + cell_width]
        off = r * extra
        soff += cell_width
        second_buf[off:off + extra] = src[soff:soff + extra]
    first = bitmap_char._replace(data=first_buf, columns=cell_width)
    second = bitmap_char._replace(data=second_buf, bearingX=0, columns=extra)
    return first, second


def current_cell():
    return CharTexture, cell_width, cell_height, baseline, underline_thickness, underline_position


@lru_cache(maxsize=8)
def missing_glyph(width):
    w = cell_width * width
    ans = bytearray(w * cell_height)
    render_missing_glyph(ans, w, cell_height)
    first, second = CharBitmap(ans, 0, 0, 0, cell_height, w), None
    if width == 2:
        first, second = split_char_bitmap(first)
        second = create_cell_buffer(
            second, 0, 0, second.rows, 0, 0, second.columns
        )
    first = create_cell_buffer(first, 0, 0, first.rows, 0, 0, first.columns)
    return first, second


def render_cell(text=' ', bold=False, italic=False):
    # TODO: Handle non-normalizable combining chars. Probably need to use
    # harfbuzz for that
    text = unicodedata.normalize('NFC', text)[0]
    width = wcwidth(text)
    try:
        bitmap_char = render_char(text, bold, italic, width)
    except FontNotFound as err:
        safe_print('ERROR:', err, file=sys.stderr)
        return missing_glyph(width)
    second = None
    if width == 2:
        if bitmap_char.columns > cell_width:
            bitmap_char, second = split_char_bitmap(bitmap_char)
            second = place_char_in_cell(second)
        else:
            second = render_cell()[0]

    first = place_char_in_cell(bitmap_char)

    return first, second


def create_cell_buffer(
    bitmap_char, src_start_row, dest_start_row, row_count, src_start_column,
    dest_start_column, column_count
):
    src = bitmap_char.data
    src_stride = bitmap_char.columns
    dest = CharTexture()
    for r in range(row_count):
        sr, dr = src_start_column + (
            src_start_row + r
        ) * src_stride, dest_start_column + (dest_start_row + r) * cell_width
        dest[dr:dr + column_count] = src[sr:sr + column_count]
    return dest
