func (f *recordingFile) parseRecord(recordNum int) *record {
	r, ok := f.recordDecls[recordNum]
	if !ok {
		panic(fmt.Errorf("record with number %d must exist", recordNum))
	}
	fields := splitString(r, "\t")
	recType, ok := strToRecType[fields[0]]
	if !ok {
		panic(fmt.Errorf("record type %v is not recognized", fields[0]))
	}
	rec := &record{Typ: recType}
	for i := 1; i < len(fields); i++ {
		val, err := parseValueWithType(fields[i])
		if err != nil {
			panic(fmt.Errorf("error parsing %s: %v", fields[i], err))
		}
		rec.Args = append(rec.Args, val)
	}
	return rec
}
parseRecord instantiates the copyist record declaration identified by the
given number in the copyist recording file.
package copyist
import (
	"bufio"
	"bytes"
	"crypto/md5"
	"errors"
	"fmt"
	"hash"
	"io/ioutil"
	"os"
	"path"
	"strconv"
	"strings"
)
type hashValue [md5.Size]byte
type recordingFile struct {
	pathName string
	recordDecls map[int]string
	recordingDecls map[string]string
	addRecordings map[string]recording
	md5Hasher hash.Hash
	scratch bytes.Buffer
}
func newRecordingFile(pathName string) *recordingFile {
	return &recordingFile{pathName: pathName, md5Hasher: md5.New()}
}
func (f *recordingFile) GetRecording(recordingName string) recording {
	recordingDecl, ok := f.recordingDecls[recordingName]
	if !ok {
		return nil
	}
	nums := f.parseRecordingDecl(recordingDecl)
	recording := make(recording, len(nums))
	for i := range nums {
		rec := f.parseRecord(nums[i])
		recording[i] = rec
	}
	return recording
}
func (f *recordingFile) AddRecording(recordingName string, newRecording recording) {
	if f.addRecordings == nil {
		f.addRecordings = make(map[string]recording)
	}
	f.addRecordings[recordingName] = newRecording
}
func (f *recordingFile) WriteRecordingFile() {
	outRecordDecls := make([]string, 0, len(f.recordingDecls)+len(f.addRecordings))
	outRecordingDecls := make(map[string]string)
	hashToNumMap := make(map[hashValue]int)
	addRecordDecl := func(recordDecl string) int {
		if len(recordDecl) > MaxRecordingSize {
			panic(errors.New("recording exceeds copyist.MaxRecordingSize and cannot be written"))
		}
		hashVal := f.hashStr(recordDecl)
		num, ok := hashToNumMap[hashVal]
		if !ok {
			num = len(outRecordDecls)
			hashToNumMap[hashVal] = num
			outRecordDecls = append(outRecordDecls, recordDecl)
		}
		return num
	}
	formatRecording := func(recordNums []int) string {
		f.scratch.Reset()
		for i, num := range recordNums {
			if i != 0 {
				f.scratch.WriteByte(',')
			}
			f.scratch.WriteString(strconv.Itoa(num + 1))
		}
		return f.scratch.String()
	}
	for recordingName, recordingDecl := range f.recordingDecls {
		if _, ok := f.addRecordings[recordingName]; ok {
			continue
		}
		oldRecordNums := f.parseRecordingDecl(recordingDecl)
		newRecordNums := make([]int, len(oldRecordNums))
		for i, num := range oldRecordNums {
			recordDecl, ok := f.recordDecls[num]
			if !ok {
				panic(fmt.Errorf("record with number %d must exist", num))
			}
			newRecordNums[i] = addRecordDecl(recordDecl)
		}
		outRecordingDecls[recordingName] = formatRecording(newRecordNums)
	}
	for recordingName, recording := range f.addRecordings {
		newRecordNums := make([]int, len(recording))
		for i, record := range recording {
			newRecordNums[i] = addRecordDecl(f.formatRecord(record))
		}
		outRecordingDecls[recordingName] = formatRecording(newRecordNums)
	}
	f.scratch.Reset()
	for num, recordDecl := range outRecordDecls {
		f.scratch.WriteString(strconv.Itoa(num + 1))
		f.scratch.WriteByte('=')
		f.scratch.WriteString(recordDecl)
		f.scratch.WriteByte('\n')
	}
	f.scratch.WriteByte('\n')
	for recordingName, recordingDecl := range outRecordingDecls {
		f.scratch.WriteString(strconv.Quote(recordingName))
		f.scratch.WriteByte('=')
		f.scratch.WriteString(recordingDecl)
		f.scratch.WriteByte('\n')
	}
	dirName := path.Dir(f.pathName)
	if _, err := os.Stat(dirName); os.IsNotExist(err) {
		if err := os.MkdirAll(dirName, 0777); err != nil {
			panic(err)
		}
	}
	if err := ioutil.WriteFile(f.pathName, f.scratch.Bytes(), 0666); err != nil {
		panic(err)
	}
}
func (f *recordingFile) Parse() error {
	file, err := os.Open(f.pathName)
	if err != nil {
		return fmt.Errorf("error opening copyist recording file: %v", err)
	}
	defer file.Close()
	recordDecls := make(map[int]string)
	recordingDecls := make(map[string]string)
	scanner := bufio.NewScanner(file)
	scanner.Buffer(nil, MaxRecordingSize)
	for scanner.Scan() {
		text := scanner.Text()
		if len(text) == 0 {
			continue
		}
		if text[0] != '"' {
			index := strings.Index(text, "=")
			if index == -1 {
				return fmt.Errorf("expected equals: %s", text)
			}
			recordNum, err := strconv.Atoi(text[:index])
			if err != nil {
				return fmt.Errorf("expected record number: %s", text)
			}
			recordDecls[recordNum-1] = text[index+1:]
		} else {
			index := strings.LastIndex(text, "=")
			if index == -1 {
				return fmt.Errorf("expected equals: %s", text)
			}
			recordingName, err := strconv.Unquote(text[:index])
			if err != nil {
				return err
			}
			recordingDecl := text[index+1:]
			recordingDecls[recordingName] = recordingDecl
		}
	}
	if err := scanner.Err(); err != nil {
		if err == bufio.ErrTooLong {
			err = errors.New("recording exceeds copyist.MaxRecordingSize and cannot be read")
		}
		return err
	}
	f.recordDecls = recordDecls
	f.recordingDecls = recordingDecls
	return nil
}
func (f *recordingFile) parseRecordingDecl(decl string) []int {
	numStrs := splitString(decl, ",")
	nums := make([]int, len(numStrs))
	for i := range numStrs {
		num, err := strconv.Atoi(numStrs[i])
		if err != nil {
			panic(err)
		}
		nums[i] = num - 1
	}
	return nums
}
func (f *recordingFile) formatRecord(record *record) string {
	f.scratch.Reset()
	f.scratch.WriteString(record.Typ.String())
	for _, arg := range record.Args {
		f.scratch.WriteByte('\t')
		f.scratch.WriteString(formatValueWithType(arg))
	}
	return f.scratch.String()
}