module patch.boxdraw;

import deimos.X11.X : Colormap;
import deimos.X11.Xlib : Display, Visual;
import xft_types : XftDraw, XftColor, XftGlyphFontSpec, XRenderColor;
import config : boxdraw, boxdraw_bold, boxdraw_braille;
import st : Glyph, Rune, GlyphAttribute;
import std.algorithm : min, max;

// External Xft functions
extern(C) {
    void XftDrawRect(XftDraw* d, const(XftColor)* color, int x, int y, uint width, uint height);
    int XftColorAllocValue(Display* dpy, Visual* visual, Colormap cmap, const(XRenderColor)* color, XftColor* result);
    void XftColorFree(Display* dpy, Visual* visual, Colormap cmap, XftColor* color);
}

// Rounded non-negative integers division of n / d
private int DIV(int n, int d) { return (n + d / 2) / d; }

// Module variables
private Display* xdpy;
private Colormap xcmap;
private XftDraw* xd;
private Visual* xvis;

// Box drawing constants
enum {
    BDL = (1<<8),   // Box Draw Lines (light/double/heavy)
    BDA = (1<<9),   // Box Draw Arc (light)
    
    BBD = (1<<10),  // Box Block Down (lower) X/8
    BBL = (2<<10),  // Box Block Left X/8
    BBU = (3<<10),  // Box Block Upper X/8
    BBR = (4<<10),  // Box Block Right X/8
    BBQ = (5<<10),  // Box Block Quadrants
    BRL = (6<<10),  // Box Braille (data is lower byte of U28XX)
    
    BBS = (1<<14),  // Box Block Shades
    BDB = (1<<15),  // Box Draw is Bold
}

// Light/Double/Heavy x Left/Up/Right/Down/Horizontal/Vertical
enum {
    LL = (1<<0),
    LU = (1<<1),
    LR = (1<<2),
    LD = (1<<3),
    LH = LL+LR,
    LV = LU+LD,
    
    DL = (1<<4),
    DU = (1<<5),
    DR = (1<<6),
    DD = (1<<7),
    DH = DL+DR,
    DV = DU+DD,
    
    HL = LL+DL,
    HU = LU+DU,
    HR = LR+DR,
    HD = LD+DD,
    HH = HL+HR,
    HV = HU+HD,
}

// Quadrants Top/Bottom x Left/Right
enum {
    TL = (1<<0),
    TR = (1<<1),
    BL = (1<<2),
    BR = (1<<3),
}

// Data for U+2500 - U+259F except dashes/diagonals
__gshared immutable ushort[256] boxdata = [
    // light lines
    0x00: BDL + LH,       // light horizontal
    0x02: BDL + LV,       // light vertical
    0x0c: BDL + LD + LR,  // light down and right
    0x10: BDL + LD + LL,  // light down and left
    0x14: BDL + LU + LR,  // light up and right
    0x18: BDL + LU + LL,  // light up and left
    0x1c: BDL + LV + LR,  // light vertical and right
    0x24: BDL + LV + LL,  // light vertical and left
    0x2c: BDL + LH + LD,  // light horizontal and down
    0x34: BDL + LH + LU,  // light horizontal and up
    0x3c: BDL + LV + LH,  // light vertical and horizontal
    0x74: BDL + LL,       // light left
    0x75: BDL + LU,       // light up
    0x76: BDL + LR,       // light right
    0x77: BDL + LD,       // light down

    // heavy [+light] lines
    0x01: BDL + HH,
    0x03: BDL + HV,
    0x0d: BDL + HR + LD,
    0x0e: BDL + HD + LR,
    0x0f: BDL + HD + HR,
    0x11: BDL + HL + LD,
    0x12: BDL + HD + LL,
    0x13: BDL + HD + HL,
    0x15: BDL + HR + LU,
    0x16: BDL + HU + LR,
    0x17: BDL + HU + HR,
    0x19: BDL + HL + LU,
    0x1a: BDL + HU + LL,
    0x1b: BDL + HU + HL,
    0x1d: BDL + HR + LV,
    0x1e: BDL + HU + LD + LR,
    0x1f: BDL + HD + LR + LU,
    0x20: BDL + HV + LR,
    0x21: BDL + HU + HR + LD,
    0x22: BDL + HD + HR + LU,
    0x23: BDL + HV + HR,
    0x25: BDL + HL + LV,
    0x26: BDL + HU + LD + LL,
    0x27: BDL + HD + LU + LL,
    0x28: BDL + HV + LL,
    0x29: BDL + HU + HL + LD,
    0x2a: BDL + HD + HL + LU,
    0x2b: BDL + HV + HL,
    0x2d: BDL + HL + LD + LR,
    0x2e: BDL + HR + LL + LD,
    0x2f: BDL + HH + LD,
    0x30: BDL + HD + LH,
    0x31: BDL + HD + HL + LR,
    0x32: BDL + HR + HD + LL,
    0x33: BDL + HH + HD,
    0x35: BDL + HL + LU + LR,
    0x36: BDL + HR + LU + LL,
    0x37: BDL + HH + LU,
    0x38: BDL + HU + LH,
    0x39: BDL + HU + HL + LR,
    0x3a: BDL + HU + HR + LL,
    0x3b: BDL + HH + HU,
    0x3d: BDL + HL + LV + LR,
    0x3e: BDL + HR + LV + LL,
    0x3f: BDL + HH + LV,
    0x40: BDL + HU + LH + LD,
    0x41: BDL + HD + LH + LU,
    0x42: BDL + HV + LH,
    0x43: BDL + HU + HL + LD + LR,
    0x44: BDL + HU + HR + LD + LL,
    0x45: BDL + HD + HL + LU + LR,
    0x46: BDL + HD + HR + LU + LL,
    0x47: BDL + HH + HU + LD,
    0x48: BDL + HH + HD + LU,
    0x49: BDL + HV + HL + LR,
    0x4a: BDL + HV + HR + LL,
    0x4b: BDL + HV + HH,
    0x78: BDL + HL,
    0x79: BDL + HU,
    0x7a: BDL + HR,
    0x7b: BDL + HD,
    0x7c: BDL + HR + LL,
    0x7d: BDL + HD + LU,
    0x7e: BDL + HL + LR,
    0x7f: BDL + HU + LD,

    // double [+light] lines
    0x50: BDL + DH,
    0x51: BDL + DV,
    0x52: BDL + DR + LD,
    0x53: BDL + DD + LR,
    0x54: BDL + DR + DD,
    0x55: BDL + DL + LD,
    0x56: BDL + DD + LL,
    0x57: BDL + DL + DD,
    0x58: BDL + DR + LU,
    0x59: BDL + DU + LR,
    0x5a: BDL + DU + DR,
    0x5b: BDL + DL + LU,
    0x5c: BDL + DU + LL,
    0x5d: BDL + DL + DU,
    0x5e: BDL + DR + LV,
    0x5f: BDL + DV + LR,
    0x60: BDL + DV + DR,
    0x61: BDL + DL + LV,
    0x62: BDL + DV + LL,
    0x63: BDL + DV + DL,
    0x64: BDL + DH + LD,
    0x65: BDL + DD + LH,
    0x66: BDL + DD + DH,
    0x67: BDL + DH + LU,
    0x68: BDL + DU + LH,
    0x69: BDL + DH + DU,
    0x6a: BDL + DH + LV,
    0x6b: BDL + DV + LH,
    0x6c: BDL + DH + DV,

    // (light) arcs
    0x6d: BDA + LD + LR,
    0x6e: BDA + LD + LL,
    0x6f: BDA + LU + LL,
    0x70: BDA + LU + LR,

    // Lower (Down) X/8 block (data is 8 - X)
    0x81: BBD + 7, 0x82: BBD + 6, 0x83: BBD + 5, 0x84: BBD + 4,
    0x85: BBD + 3, 0x86: BBD + 2, 0x87: BBD + 1, 0x88: BBD + 0,

    // Left X/8 block (data is X)
    0x89: BBL + 7, 0x8a: BBL + 6, 0x8b: BBL + 5, 0x8c: BBL + 4,
    0x8d: BBL + 3, 0x8e: BBL + 2, 0x8f: BBL + 1,

    // upper 1/2 (4/8), 1/8 block (X), right 1/2, 1/8 block (8-X)
    0x80: BBU + 4, 0x94: BBU + 1,
    0x90: BBR + 4, 0x95: BBR + 7,

    // Quadrants
    0x96: BBQ + BL,
    0x97: BBQ + BR,
    0x98: BBQ + TL,
    0x99: BBQ + TL + BL + BR,
    0x9a: BBQ + TL + BR,
    0x9b: BBQ + TL + TR + BL,
    0x9c: BBQ + TL + TR + BR,
    0x9d: BBQ + TR,
    0x9e: BBQ + BL + TR,
    0x9f: BBQ + BL + TR + BR,

    // Shades, data is an alpha value in 25% units (1/4, 1/2, 3/4)
    0x91: BBS + 1, 0x92: BBS + 2, 0x93: BBS + 3,
];

// Public API
void boxdraw_xinit(Display* dpy, Colormap cmap, XftDraw* draw, Visual* vis) {
    xdpy = dpy;
    xcmap = cmap;
    xd = draw;
    xvis = vis;
}

int isboxdraw(Rune u) {
    Rune block_ = u & ~0xff;
    return (boxdraw && block_ == 0x2500 && boxdata[cast(ubyte)u]) ||
           (boxdraw_braille && block_ == 0x2800);
}

ushort boxdrawindex(const(Glyph)* g) {
    if (boxdraw_braille && (g.u & ~0xff) == 0x2800)
        return BRL | cast(ubyte)g.u;
    if (boxdraw_bold && (g.mode & GlyphAttribute.BOLD))
        return BDB | boxdata[cast(ubyte)g.u];
    return boxdata[cast(ubyte)g.u];
}

void drawboxes(int x, int y, int cw, int ch, XftColor* fg, XftColor* bg,
               const(XftGlyphFontSpec)* specs, int len) {
    for ( ; len-- > 0; x += cw, specs++)
        drawbox(x, y, cw, ch, fg, bg, cast(ushort)specs.glyph);
}

// Implementation
private void drawbox(int x, int y, int w, int h, XftColor* fg, XftColor* bg, ushort bd) {
    ushort cat = bd & ~(BDB | 0xff);  // mask out bold and data
    if (bd & (BDL | BDA)) {
        // lines (light/double/heavy/arcs)
        drawboxlines(x, y, w, h, fg, bd);
        
    } else if (cat == BBD) {
        // lower (8-X)/8 block
        int d = DIV(cast(ubyte)bd * h, 8);
        XftDrawRect(xd, fg, x, y + d, w, h - d);
        
    } else if (cat == BBU) {
        // upper X/8 block
        XftDrawRect(xd, fg, x, y, w, DIV(cast(ubyte)bd * h, 8));
        
    } else if (cat == BBL) {
        // left X/8 block
        XftDrawRect(xd, fg, x, y, DIV(cast(ubyte)bd * w, 8), h);
        
    } else if (cat == BBR) {
        // right (8-X)/8 block
        int d = DIV(cast(ubyte)bd * w, 8);
        XftDrawRect(xd, fg, x + d, y, w - d, h);
        
    } else if (cat == BBQ) {
        // Quadrants
        int w2 = DIV(w, 2), h2 = DIV(h, 2);
        if (bd & TL)
            XftDrawRect(xd, fg, x, y, w2, h2);
        if (bd & TR)
            XftDrawRect(xd, fg, x + w2, y, w - w2, h2);
        if (bd & BL)
            XftDrawRect(xd, fg, x, y + h2, w2, h - h2);
        if (bd & BR)
            XftDrawRect(xd, fg, x + w2, y + h2, w - w2, h - h2);
            
    } else if (bd & BBS) {
        // Shades - data is 1/2/3 for 25%/50%/75% alpha, respectively
        int d = cast(ubyte)bd;
        XftColor xfc;
        XRenderColor xrc;
        xrc.alpha = 0xffff;
        
        xrc.red = cast(ushort)DIV(fg.color.red * d + bg.color.red * (4 - d), 4);
        xrc.green = cast(ushort)DIV(fg.color.green * d + bg.color.green * (4 - d), 4);
        xrc.blue = cast(ushort)DIV(fg.color.blue * d + bg.color.blue * (4 - d), 4);
        
        XftColorAllocValue(xdpy, xvis, xcmap, &xrc, &xfc);
        XftDrawRect(xd, &xfc, x, y, w, h);
        XftColorFree(xdpy, xvis, xcmap, &xfc);
        
    } else if (cat == BRL) {
        // braille, each data bit corresponds to one dot at 2x4 grid
        int w1 = DIV(w, 2);
        int h1 = DIV(h, 4), h2 = DIV(h, 2), h3 = DIV(3 * h, 4);
        
        if (bd & 1)   XftDrawRect(xd, fg, x, y, w1, h1);
        if (bd & 2)   XftDrawRect(xd, fg, x, y + h1, w1, h2 - h1);
        if (bd & 4)   XftDrawRect(xd, fg, x, y + h2, w1, h3 - h2);
        if (bd & 8)   XftDrawRect(xd, fg, x + w1, y, w - w1, h1);
        if (bd & 16)  XftDrawRect(xd, fg, x + w1, y + h1, w - w1, h2 - h1);
        if (bd & 32)  XftDrawRect(xd, fg, x + w1, y + h2, w - w1, h3 - h2);
        if (bd & 64)  XftDrawRect(xd, fg, x, y + h3, w1, h - h3);
        if (bd & 128) XftDrawRect(xd, fg, x + w1, y + h3, w - w1, h - h3);
    }
}

private void drawboxlines(int x, int y, int w, int h, XftColor* fg, ushort bd) {
    // s: stem thickness. width/8 roughly matches underscore thickness.
    // We draw bold as 1.5 * normal-stem and at least 1px thicker.
    // doubles draw at least 3px, even when w or h < 3. bold needs 6px.
    int mwh = min(w, h);
    int base_s = max(1, DIV(mwh, 8));
    int bold = (bd & BDB) && mwh >= 6;  // possibly ignore boldness
    int s = bold ? max(base_s + 1, DIV(3 * base_s, 2)) : base_s;
    int w2 = DIV(w - s, 2), h2 = DIV(h - s, 2);
    // the s-by-s square (x + w2, y + h2, s, s) is the center texel.
    // The base length (per direction till edge) includes this square.
    
    int light = bd & (LL | LU | LR | LD);
    int double_ = bd & (DL | DU | DR | DD);
    
    if (light) {
        // d: additional (negative) length to not-draw the center
        // texel - at arcs and avoid drawing inside (some) doubles
        int arc = bd & BDA;
        int multi_light = light & (light - 1);
        int multi_double = double_ & (double_ - 1);
        // light crosses double only at DH+LV, DV+LH (ref. shapes)
        int d = arc || (multi_double && !multi_light) ? -s : 0;
        
        if (bd & LL)
            XftDrawRect(xd, fg, x, y + h2, w2 + s + d, s);
        if (bd & LU)
            XftDrawRect(xd, fg, x + w2, y, s, h2 + s + d);
        if (bd & LR)
            XftDrawRect(xd, fg, x + w2 - d, y + h2, w - w2 + d, s);
        if (bd & LD)
            XftDrawRect(xd, fg, x + w2, y + h2 - d, s, h - h2 + d);
    }
    
    // double lines - also align with light to form heavy when combined
    if (double_) {
        /*
        * going clockwise, for each double-ray: p is additional length
        * to the single-ray nearer to the previous direction, and n to
        * the next. p and n adjust from the base length to lengths
        * which consider other doubles - shorter to avoid intersections
        * (p, n), or longer to draw the far-corner texel (n).
        */
        int dl = bd & DL, du = bd & DU, dr = bd & DR, dd = bd & DD;
        if (dl) {
            int p = dd ? -s : 0, n = du ? -s : dd ? s : 0;
            XftDrawRect(xd, fg, x, y + h2 + s, w2 + s + p, s);
            XftDrawRect(xd, fg, x, y + h2 - s, w2 + s + n, s);
        }
        if (du) {
            int p = dl ? -s : 0, n = dr ? -s : dl ? s : 0;
            XftDrawRect(xd, fg, x + w2 - s, y, s, h2 + s + p);
            XftDrawRect(xd, fg, x + w2 + s, y, s, h2 + s + n);
        }
        if (dr) {
            int p = du ? -s : 0, n = dd ? -s : du ? s : 0;
            XftDrawRect(xd, fg, x + w2 - p, y + h2 - s, w - w2 + p, s);
            XftDrawRect(xd, fg, x + w2 - n, y + h2 + s, w - w2 + n, s);
        }
        if (dd) {
            int p = dr ? -s : 0, n = dl ? -s : dr ? s : 0;
            XftDrawRect(xd, fg, x + w2 + s, y + h2 - p, s, h - h2 + p);
            XftDrawRect(xd, fg, x + w2 - s, y + h2 - n, s, h - h2 + n);
        }
    }
}