<commit_before>package repo

import (
	"bytes"
	"encoding/hex"
	"html/template"
	"math/rand"
	"strconv"
	"time"

	"github.com/flimzy/log"
	"github.com/pkg/errors"
	"golang.org/x/net/html"

	"github.com/flimzy/go-pouchdb"

	"github.com/FlashbackSRS/flashback-model"
	"github.com/FlashbackSRS/flashback/util"
)

// Card provides a convenient interface to fb.Card and dependencies
type Card struct {
	*fb.Card
	db   *DB
	note *Note
}

// Note returns the card's associated Note
func (c *Card) Note() (*Note, error) {
	if err := c.fetchNote(); err != nil {
		return nil, errors.Wrap(err, "Error fetching note for Note()")
	}
	return c.note, nil
}

func (c *Card) fetchNote() error {
	if c.note != nil {
		// Nothing to do
		return nil
	}
	log.Debugf("Fetching note %s", c.NoteID())
	db, err := NewDB(c.BundleID())
	if err != nil {
		return errors.Wrap(err, "fetchNote() can't connect to bundle DB")
	}
	n := &fb.Note{}
	if err := db.Get(c.NoteID(), n, pouchdb.Options{Attachments: true}); err != nil {
		return errors.Wrapf(err, "fetchNote() can't fetch %s", c.NoteID())
	}
	c.note = &Note{
		Note: n,
		db:   db,
	}
	return nil
}

// GetCard fetches the requested card
func (u *User) GetCard(id string) (*Card, error) {
	db, err := u.DB()
	if err != nil {
		return nil, errors.Wrap(err, "Unable to connect to User DB")
	}

	card := &fb.Card{}
	if err := db.Get(id, card, pouchdb.Options{}); err != nil {
		return nil, errors.Wrap(err, "Unable to fetch requested card")
	}
	return &Card{
		Card: card,
		db:   db,
	}, nil
}

// GetNextCard gets the next card to study
func (u *User) GetNextCard() (*Card, error) {
	db, err := u.DB()
	if err != nil {
		return nil, errors.Wrap(err, "GetNextCard(): Error connecting to User DB")
	}

	doc := make(map[string][]*fb.Card)
	query := map[string]interface{}{
		"selector": map[string]string{"type": "card"},
		"limit":    1,
	}
	if err := db.Find(query, &doc); err != nil {
		return nil, errors.Wrap(err, "GetNextCard(): Error fetching card")
	}
	if len(doc["docs"]) == 0 {
		return nil, errors.New("No cards available")
	}
	return &Card{
		Card: doc["docs"][0],
		db:   db,
	}, nil
}

type cardContext struct {
	IframeID string
	Card     *Card
	Note     *Note
	Model    *Model
	Deck     *Deck
	BaseURI  string
	Fields   map[string]template.HTML
}

const (
	// Question is a card's first face
	Question = iota
	// Answer is a card's second face
	Answer
)

var faces = map[int]string{
	Question: "question",
	Answer:   "answer",
}

// Body returns the requested card face
func (c *Card) Body(face int) (string, string, error) {
	cardFace, ok := faces[face]
	if !ok {
		return "", "", errors.Errorf("Unrecognized card face %d", face)
	}
	note, err := c.Note()
	if err != nil {
		return "", "", errors.Wrap(err, "Unable to retrieve Note")
	}
	model, err := note.Model()
	if err != nil {
		return "", "", errors.Wrap(err, "Unable to retrieve Model")
	}
	tmpl, err := model.GenerateTemplate()
	if err != nil {
		return "", "", errors.Wrap(err, "Error generating template")
	}
	ctx := cardContext{
		IframeID: RandString(8),
		Card:     c,
		Note:     note,
		Model:    model,
		BaseURI:  util.BaseURI(),
		Fields:   make(map[string]template.HTML),
	}

	for i, f := range model.Fields {
		switch note.FieldValues[i].Type() {
		case fb.AnkiField, fb.TextField:
			text, e := note.FieldValues[i].Text()
			if e != nil {
				return "", "", errors.Wrap(e, "Unable to fetch text for field value")
			}
			ctx.Fields[f.Name] = template.HTML(text)
		}
	}

	htmlDoc := new(bytes.Buffer)
	if e := tmpl.Execute(htmlDoc, ctx); e != nil {
		return "", "", errors.Wrap(e, "Unable to execute template")
	}
	doc, err := html.Parse(htmlDoc)
	if err != nil {
		return "", "", errors.Wrap(err, "Unable to parse generated HTML")
	}
	body := findBody(doc)
	if body == nil {
		return "", "", errors.New("No <body> in the template output")
	}
	log.Debugf("%s", htmlDoc)

	container := findContainer(body.FirstChild, strconv.Itoa(int(c.TemplateID())), cardFace)
	if container == nil {
		return "", "", errors.Errorf("No div matching '%d' found in template output", c.TemplateID())
	}
	log.Debug("Found container: %s", container)

	// Delete unused divs
	for c := body.FirstChild; c != nil; c = body.FirstChild {
		body.RemoveChild(c)
	}
	inner := container.FirstChild
	inner.Parent = body
	body.FirstChild = inner

	newBody := new(bytes.Buffer)
	if err := html.Render(newBody, doc); err != nil {
		return "", "", errors.Wrap(err, "Error rendering new HTML")
	}

	nbString := newBody.String()
	log.Debugf("original size = %d\n", len(htmlDoc.String()))
	log.Debugf("new body size = %d\n", len(nbString))
	return nbString, ctx.IframeID, nil
}

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
	letterIdxBits = 6                    // 6 bits to represent a letter index
	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

// Random number function borrowed from http://stackoverflow.com/a/31832326/13860
var src = rand.NewSource(time.Now().UnixNano())

// RandString returns a random string of n bytes, converted to hex
func RandString(n int) string {
	b := make([]byte, n)
	// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
	for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
		if remain == 0 {
			cache, remain = src.Int63(), letterIdxMax
		}
		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
			b[i] = letterBytes[idx]
			i--
		}
		cache >>= letterIdxBits
		remain--
	}
<commit_msg>Minor reorg, in preparation for adding unit tests<commit_after>package repo

import (
	"bytes"
	"encoding/hex"
	"html/template"
	"io"
	"math/rand"
	"strconv"
	"time"

	"github.com/flimzy/go-pouchdb"
	"github.com/flimzy/log"
	"github.com/pkg/errors"
	"golang.org/x/net/html"

	"github.com/FlashbackSRS/flashback-model"
	"github.com/FlashbackSRS/flashback/util"
)

// Card provides a convenient interface to fb.Card and dependencies
type Card struct {
	*fb.Card
	db   *DB
	note *Note
}

// Note returns the card's associated Note
func (c *Card) Note() (*Note, error) {
	if err := c.fetchNote(); err != nil {
		return nil, errors.Wrap(err, "Error fetching note for Note()")
	}
	return c.note, nil
}

func (c *Card) fetchNote() error {
	if c.note != nil {
		// Nothing to do
		return nil
	}
	log.Debugf("Fetching note %s", c.NoteID())
	db, err := NewDB(c.BundleID())
	if err != nil {
		return errors.Wrap(err, "fetchNote() can't connect to bundle DB")
	}
	n := &fb.Note{}
	if err := db.Get(c.NoteID(), n, pouchdb.Options{Attachments: true}); err != nil {
		return errors.Wrapf(err, "fetchNote() can't fetch %s", c.NoteID())
	}
	c.note = &Note{
		Note: n,
		db:   db,
	}
	return nil
}

// GetCard fetches the requested card
func (u *User) GetCard(id string) (*Card, error) {
	db, err := u.DB()
	if err != nil {
		return nil, errors.Wrap(err, "Unable to connect to User DB")
	}

	card := &fb.Card{}
	if err := db.Get(id, card, pouchdb.Options{}); err != nil {
		return nil, errors.Wrap(err, "Unable to fetch requested card")
	}
	return &Card{
		Card: card,
		db:   db,
	}, nil
}

// GetNextCard gets the next card to study
func (u *User) GetNextCard() (*Card, error) {
	db, err := u.DB()
	if err != nil {
		return nil, errors.Wrap(err, "GetNextCard(): Error connecting to User DB")
	}

	doc := make(map[string][]*fb.Card)
	query := map[string]interface{}{
		"selector": map[string]string{"type": "card"},
		"limit":    1,
	}
	if err := db.Find(query, &doc); err != nil {
		return nil, errors.Wrap(err, "GetNextCard(): Error fetching card")
	}
	if len(doc["docs"]) == 0 {
		return nil, errors.New("No cards available")
	}
	return &Card{
		Card: doc["docs"][0],
		db:   db,
	}, nil
}

type cardContext struct {
	IframeID string
	Card     *Card
	Note     *Note
	Model    *Model
	Deck     *Deck
	BaseURI  string
	Fields   map[string]template.HTML
}

const (
	// Question is a card's first face
	Question = iota
	// Answer is a card's second face
	Answer
)

var faces = map[int]string{
	Question: "question",
	Answer:   "answer",
}

// Body returns the requested card face
func (c *Card) Body(face int) (string, string, error) {
	note, err := c.Note()
	if err != nil {
		return "", "", errors.Wrap(err, "Unable to retrieve Note")
	}
	model, err := note.Model()
	if err != nil {
		return "", "", errors.Wrap(err, "Unable to retrieve Model")
	}
	tmpl, err := model.GenerateTemplate()
	if err != nil {
		return "", "", errors.Wrap(err, "Error generating template")
	}
	ctx := cardContext{
		IframeID: RandString(8),
		Card:     c,
		Note:     note,
		Model:    model,
		BaseURI:  util.BaseURI(),
		Fields:   make(map[string]template.HTML),
	}

	for i, f := range model.Fields {
		switch note.FieldValues[i].Type() {
		case fb.AnkiField, fb.TextField:
			text, e := note.FieldValues[i].Text()
			if e != nil {
				return "", "", errors.Wrap(e, "Unable to fetch text for field value")
			}
			ctx.Fields[f.Name] = template.HTML(text)
		}
	}

	htmlDoc := new(bytes.Buffer)
	if e := tmpl.Execute(htmlDoc, ctx); e != nil {
		return "", "", errors.Wrap(e, "Unable to execute template")
	}
	newBody, err := c.prepareBody(face, htmlDoc)
	if err != nil {
		return "", "", errors.Wrap(err, "prepare body")
	}

	nbString := string(newBody)
	log.Debugf("original size = %d\n", len(htmlDoc.String()))
	log.Debugf("new body size = %d\n", len(nbString))
	return nbString, ctx.IframeID, nil
}

func (c *Card) prepareBody(face int, htmlDoc io.Reader) ([]byte, error) {
	cardFace, ok := faces[face]
	if !ok {
		return nil, errors.Errorf("Unrecognized card face %d", face)
	}
	doc, err := html.Parse(htmlDoc)
	if err != nil {
		return nil, errors.Wrap(err, "Unable to parse generated HTML")
	}
	body := findBody(doc)
	if body == nil {
		return nil, errors.New("No <body> in the template output")
	}
	log.Debugf("%s", htmlDoc)

	container := findContainer(body.FirstChild, strconv.Itoa(int(c.TemplateID())), cardFace)
	if container == nil {
		return nil, errors.Errorf("No div matching '%d' found in template output", c.TemplateID())
	}
	log.Debug("Found container: %s", container)

	// Delete unused divs
	for c := body.FirstChild; c != nil; c = body.FirstChild {
		body.RemoveChild(c)
	}
	inner := container.FirstChild
	inner.Parent = body
	body.FirstChild = inner

	newBody := new(bytes.Buffer)
	if err := html.Render(newBody, doc); err != nil {
		return nil, errors.Wrap(err, "Error rendering new HTML")
	}
	return newBody.Bytes(), nil
}

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
	letterIdxBits = 6                    // 6 bits to represent a letter index
	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

// Random number function borrowed from http://stackoverflow.com/a/31832326/13860
var src = rand.NewSource(time.Now().UnixNano())

// RandString returns a random string of n bytes, converted to hex
func RandString(n int) string {
	b := make([]byte, n)
	// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
	for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
		if remain == 0 {
			cache, remain = src.Int63(), letterIdxMax
		}
		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
			b[i] = letterBytes[idx]
			i--
		}
		cache >>= letterIdxBits
		remain--
	}

{'fraction_non_alphanumeric': 0.15921616656460502, 'fraction_numerical': 0.0064736243548246, 'mean_word_length': 3.552767821585026, 'pattern_counts': {'":': 6, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1955936', 'n_tokens_mistral': 4330, 'n_tokens_neox': 4095, 'n_words': 1192}