package card

import (
	"errors"
	"image"
	"image/draw"
	"math"
	"os"
	"os/exec"
	"runtime"
	"strconv"
	"strings"
	"sync"

	"golang.org/x/image/font"
	"golang.org/x/image/font/opentype"
	"golang.org/x/image/font/sfnt"
	"golang.org/x/image/math/fixed"
)

// Fonts is an ordered fallback chain; the first font with a glyph wins.
// Font files are read through ReaderAt, never loaded whole.
type Fonts struct {
	chain []*sfnt.Font
	latin int // index in chain of the font for Latin text, -1 for the chain's order

	mu    sync.Mutex
	faces map[faceKey]*face
}

type faceKey struct {
	size  float64 // device px
	thick bool
}

type fontSpec struct {
	path string
	name string // substring of the full name to pick from a collection; "" takes index
	idx  int
}

var macFonts = []fontSpec{
	{path: "/System/Library/Fonts/ヒラギノ丸ゴ ProN W4.ttc", name: "ProN"},
	{path: "/System/Library/Fonts/ヒラギノ角ゴシック W4.ttc"},
	{path: "/System/Library/Fonts/Supplemental/Arial Unicode.ttf"},
}

// Rounded faces first: they suit the card. fc-match always answers, so each
// family is only taken when fontconfig returns that family.
var linuxFamilies = []string{
	"M PLUS Rounded 1c", "Zen Maru Gothic", "Kosugi Maru",
	"Noto Sans CJK JP", "Source Han Sans JP", "Source Han Sans", "IPAexGothic", "VL Gothic",
}

// LoadFonts finds Japanese-capable fonts. $CHIRAMI_FONT ("path" or
// "path:index") goes first when set.
func LoadFonts() (*Fonts, error) {
	var specs []fontSpec
	if env := os.Getenv("CHIRAMI_FONT"); env != "" {
		specs = append(specs, parseSpec(env))
	}
	if runtime.GOOS == "darwin" {
		specs = append(specs, macFonts...)
	} else {
		specs = append(specs, fontconfigSpecs()...)
	}
	fs := &Fonts{faces: map[faceKey]*face{}, latin: -1}
	if os.Getenv("CHIRAMI_FONT") == "" {
		if latin, japanese := shippedFonts(); latin != nil && japanese != nil {
			fs.chain, fs.latin = []*sfnt.Font{latin, japanese}, 0
		}
	}
	for _, s := range specs {
		if f, err := openFont(s); err == nil && renders(f) {
			fs.chain = append(fs.chain, f)
		}
	}
	if len(fs.chain) == 0 {
		return nil, errors.New("card: no Japanese font found (set CHIRAMI_FONT=/path/to/font.ttc)")
	}
	return fs, nil
}

func parseSpec(s string) fontSpec {
	if i := strings.LastIndexByte(s, ':'); i > 0 {
		if n, err := strconv.Atoi(s[i+1:]); err == nil {
			return fontSpec{path: s[:i], idx: n}
		}
	}
	return fontSpec{path: s}
}

func fontconfigSpecs() []fontSpec {
	var out []fontSpec
	seen := map[string]bool{}
	for _, fam := range linuxFamilies {
		b, err := exec.Command("fc-match", "-f", "%{family}\n%{file}\n%{index}", fam+":lang=ja").Output()
		if err != nil {
			return out
		}
		parts := strings.SplitN(string(b), "\n", 3)
		if len(parts) < 3 || !strings.Contains(parts[0], fam) || seen[parts[1]+parts[2]] {
			continue
		}
		seen[parts[1]+parts[2]] = true
		idx, _ := strconv.Atoi(strings.TrimSpace(parts[2]))
		idx &= 0xffff // the high bits name a variable font's instance
		out = append(out, fontSpec{path: parts[1], idx: idx})
	}
	return out
}

func openFont(s fontSpec) (*sfnt.Font, error) {
	f, err := os.Open(s.path)
	if err != nil {
		return nil, err
	}
	c, err := sfnt.ParseCollectionReaderAt(f)
	if err != nil {
		f.Close()
		return nil, err
	}
	idx := s.idx
	if s.name != "" {
		var buf sfnt.Buffer
		for i := 0; i < c.NumFonts(); i++ {
			fnt, err := c.Font(i)
			if err != nil {
				continue
			}
			if n, _ := fnt.Name(&buf, sfnt.NameIDFull); strings.Contains(n, s.name) {
				idx = i
				break
			}
		}
	}
	if idx >= c.NumFonts() {
		idx = 0
	}
	return c.Font(idx)
}

// renders reports whether f can draw a Japanese glyph: x/image cannot draw
// CFF2 outlines, which variable CJK fonts such as Noto Sans CJK VF use.
func renders(f *sfnt.Font) bool {
	ff, err := opentype.NewFace(f, &opentype.FaceOptions{Size: 16, DPI: 72})
	if err != nil {
		return false
	}
	defer ff.Close()
	_, _, _, _, ok := ff.Glyph(fixed.Point26_6{}, 'あ')
	return ok
}

// thickGamma darkens anti-aliased edges of small text on low-density
// displays: light text on the dark card loses its strokes to half-covered
// pixels at 1x. Text from 24 device px up keeps its true coverage.
const thickGamma = 0.7

// face is one font at one device size, with its glyph masks cached.
type face struct {
	fonts  *Fonts
	size   float64
	thick  bool
	faces  []font.Face // one per chain font, created lazily
	glyphs map[glyphKey]*glyph
	ascent fixed.Int26_6
	height fixed.Int26_6
}

// glyphs are rasterised at a quarter-pixel horizontal phase, so text keeps
// its spacing at 1x instead of every glyph snapping to a whole pixel
const phases = 4

type glyphKey struct {
	r     rune
	phase int
}

type glyph struct {
	mask *image.Alpha // nil for blank glyphs
	off  image.Point  // mask origin relative to the dot
	adv  fixed.Int26_6
}

// face returns the fonts at a device size; thick darkens small text, for
// low-density displays.
func (fs *Fonts) face(sizePx float64, thick bool) *face {
	fs.mu.Lock()
	defer fs.mu.Unlock()
	k := faceKey{sizePx, thick && sizePx < 24}
	if f := fs.faces[k]; f != nil {
		return f
	}
	f := &face{fonts: fs, size: sizePx, thick: k.thick, faces: make([]font.Face, len(fs.chain)), glyphs: map[glyphKey]*glyph{}}
	// line metrics come from the Japanese font, which the layout is made for
	main := 0
	if fs.latin == 0 && len(fs.chain) > 1 {
		main = 1
	}
	m := f.chainFace(main).Metrics()
	f.ascent, f.height = m.Ascent, m.Ascent+m.Descent
	fs.faces[k] = f
	return f
}

func (f *face) chainFace(i int) font.Face {
	if f.faces[i] == nil {
		ff, err := opentype.NewFace(f.fonts.chain[i], &opentype.FaceOptions{Size: f.size, DPI: 72, Hinting: font.HintingNone})
		if err != nil {
			panic(err) // only fails for invalid options
		}
		f.faces[i] = ff
	}
	return f.faces[i]
}

func (f *face) glyph(r rune, phase int) *glyph {
	k := glyphKey{r, phase}
	if g := f.glyphs[k]; g != nil {
		return g
	}
	g := &glyph{}
	var buf sfnt.Buffer
	for i, fnt := range f.fonts.chain {
		if i == f.fonts.latin && r >= 0x2E80 {
			continue // the Latin font draws Latin only, even if it has kana
		}
		if gi, err := fnt.GlyphIndex(&buf, r); err != nil || gi == 0 {
			if i < len(f.fonts.chain)-1 {
				continue
			}
		}
		ff := f.chainFace(i)
		dr, mask, mp, adv, ok := ff.Glyph(fixed.Point26_6{X: fixed.Int26_6(phase * 64 / phases)}, r)
		if !ok {
			continue
		}
		g.adv = adv
		if !dr.Empty() {
			// opentype reuses its mask buffer, so keep a copy.
			a := image.NewAlpha(image.Rect(0, 0, dr.Dx(), dr.Dy()))
			draw.Draw(a, a.Bounds(), mask, mp, draw.Src)
			if f.thick {
				for i, v := range a.Pix {
					a.Pix[i] = uint8(255*math.Pow(float64(v)/255, thickGamma) + 0.5)
				}
			}
			g.mask, g.off = a, dr.Min
		}
		break
	}
	if len(f.glyphs) > 4096 {
		clear(f.glyphs)
	}
	f.glyphs[k] = g
	return g
}

// width of s in device px (26.6).
func (f *face) width(s string) fixed.Int26_6 {
	var w fixed.Int26_6
	for _, r := range s {
		w += f.glyph(r, 0).adv
	}
	return w
}

// draw s with its baseline at (x, y) device px and returns the advance.
func (f *face) draw(dst *image.RGBA, x, y fixed.Int26_6, s string, src image.Image) fixed.Int26_6 {
	start := x
	for _, r := range s {
		g := f.glyph(r, int(x&63)*phases/64)
		if g.mask != nil {
			p := image.Pt(x.Floor(), y.Round()).Add(g.off)
			draw.DrawMask(dst, g.mask.Bounds().Add(p), src, image.Point{}, g.mask, image.Point{}, draw.Over)
		}
		x += g.adv
	}
	return x - start
}
