A library for working with Pijul repositories in Go
package pijul

import (
	"fmt"
	"io"
)

type EdgeFlags byte

const (
	EdgeFlagsBlock   EdgeFlags = 1
	EdgeFlagsPseudo            = 4
	EdgeFlagsFolder            = 16
	EdgeFlagsParent            = 32
	EdgeFlagsDeleted           = 128
)

func edgeFlags(data []byte) ([]byte, EdgeFlags, error) {
	if len(data) < 1 {
		return data, 0, io.ErrUnexpectedEOF
	}
	return data[1:], EdgeFlags(data[0]), nil
}

type ChangePosition uint64

func changePosition(data []byte) ([]byte, ChangePosition, error) {
	return mapValue(uint64LE, func(v uint64) ChangePosition {
		return ChangePosition(v)
	})(data)
}

type Position struct {
	Change OptionalHash
	Pos    ChangePosition
}

type OptionalHash struct {
	Valid bool
	Hash
}

func (h OptionalHash) String() string {
	if !h.Valid {
		return "None"
	}
	return fmt.Sprintf("Some(%s)", h.Hash.String())
}

func optionalHash(data []byte) ([]byte, OptionalHash, error) {
	data, hp, err := option(hash)(data)
	if err != nil {
		return data, OptionalHash{}, err
	}
	if hp == nil {
		return data, OptionalHash{}, nil
	}
	return data, OptionalHash{true, *hp}, nil
}

func position(data []byte) ([]byte, Position, error) {
	var p Position
	data, _, err := tuple(
		assign(&p.Change, optionalHash),
		assign(&p.Pos, changePosition),
	)(data)
	return data, p, err
}

type NewEdge struct {
	Previous     EdgeFlags
	Flag         EdgeFlags
	From         Position
	To           Vertex
	IntroducedBy OptionalHash
}

func newEdge(data []byte) ([]byte, NewEdge, error) {
	var ne NewEdge
	data, _, err := tuple(
		assign(&ne.Previous, edgeFlags),
		assign(&ne.Flag, edgeFlags),
		assign(&ne.From, position),
		assign(&ne.To, vertex),
		assign(&ne.IntroducedBy, optionalHash),
	)(data)
	return data, ne, err
}

type Vertex struct {
	Change OptionalHash
	Start  ChangePosition
	End    ChangePosition
}

func vertex(data []byte) ([]byte, Vertex, error) {
	var v Vertex
	data, _, err := tuple(
		assign(&v.Change, optionalHash),
		assign(&v.Start, changePosition),
		assign(&v.End, changePosition),
	)(data)
	return data, v, err
}

type Atom interface {
	isAtom()
}

func atom(data []byte) ([]byte, Atom, error) {
	data, tag, err := uint32LE(data)
	if err != nil {
		return data, nil, err
	}
	switch tag {
	case 0:
		return newVertex(data)
	case 1:
		return edgeMap(data)
	default:
		return data, nil, fmt.Errorf("unknown tag for Atom: %d", tag)
	}
}

type NewVertex struct {
	UpContext   []Position
	DownContext []Position
	Flag        EdgeFlags
	Start       ChangePosition
	End         ChangePosition
	Inode       Position
}

func (NewVertex) isAtom() {}

func newVertex(data []byte) ([]byte, NewVertex, error) {
	var nv NewVertex
	data, _, err := tuple(
		assign(&nv.UpContext, vec(position)),
		assign(&nv.DownContext, vec(position)),
		assign(&nv.Flag, edgeFlags),
		assign(&nv.Start, changePosition),
		assign(&nv.End, changePosition),
		assign(&nv.Inode, position),
	)(data)
	return data, nv, err
}

type EdgeMap struct {
	Edges []NewEdge
	Inode Position
}

func (EdgeMap) isAtom() {}

func edgeMap(data []byte) ([]byte, EdgeMap, error) {
	var em EdgeMap
	data, _, err := tuple(
		assign(&em.Edges, vec(newEdge)),
		assign(&em.Inode, position),
	)(data)
	return data, em, err
}

type Local struct {
	Path string
	Line uint64
}

func local(data []byte) ([]byte, Local, error) {
	var l Local
	data, _, err := tuple(
		assign(&l.Path, toString(lengthData(uint64LE))),
		assign(&l.Line, uint64LE),
	)(data)
	return data, l, err
}

type Hunk interface {
	atoms() []Atom
}

func hunk(data []byte) ([]byte, Hunk, error) {
	data, tag, err := uint32LE(data)
	if err != nil {
		return data, nil, err
	}
	switch tag {
	case 0:
		return fileMove(data)
	case 1:
		return fileDel(data)
	case 2:
		return fileUndel(data)
	case 3:
		return fileAdd(data)
	case 4:
		return solveNameConflict(data)
	case 6:
		return edit(data)
	case 7:
		return replacement(data)
	case 8:
		return solveOrderConflict(data)
	case 10:
		return resurrectZombies(data)
	case 11:
		return addRoot(data)
	default:
		return data, nil, fmt.Errorf("unknown tag for Hunk: %d", tag)
	}
}

func hunkV4(data []byte) ([]byte, Hunk, error) {
	data, tag, err := uint32LE(data)
	if err != nil {
		return data, nil, err
	}
	switch tag {
	case 0:
		return fileMove(data)
	case 1:
		return fileDelV4(data)
	case 2:
		return fileUndelV4(data)
	case 3:
		return fileAddV4(data)
	case 4:
		return solveNameConflict(data)
	case 6:
		return editV4(data)
	case 7:
		return replacementV4(data)
	case 8:
		return solveOrderConflict(data)
	case 10:
		return resurrectZombiesV4(data)
	default:
		return data, nil, fmt.Errorf("unknown tag for Hunk V4: %d", tag)
	}
}

type FileAdd struct {
	AddName  Atom
	AddInode Atom
	Contents Atom
	Path     string
	Encoding string
}

func (f FileAdd) atoms() []Atom {
	if f.Contents == nil {
		return []Atom{f.AddName, f.AddInode}
	}
	return []Atom{f.AddName, f.AddInode, f.Contents}
}

func fileAdd(data []byte) ([]byte, FileAdd, error) {
	var f FileAdd
	data, _, err := tuple(
		assign(&f.AddName, atom),
		assign(&f.AddInode, atom),
		assign(&f.Contents, mapValue(option(atom), func(p *Atom) Atom {
			if p == nil {
				return nil
			}
			return *p
		})),
		assign(&f.Path, toString(lengthData(uint64LE))),
		assign(&f.Encoding, mapValue(option(toString(lengthData(uint64LE))), func(p *string) string {
			if p == nil {
				return ""
			}
			return *p
		})),
	)(data)
	return data, f, err
}

func fileAddV4(data []byte) ([]byte, FileAdd, error) {
	var f FileAdd
	f.Encoding = "UTF-8"
	data, _, err := tuple(
		assign(&f.AddName, atom),
		assign(&f.AddInode, atom),
		assign(&f.Contents, mapValue(option(atom), func(p *Atom) Atom {
			if p == nil {
				return nil
			}
			return *p
		})),
		assign(&f.Path, toString(lengthData(uint64LE))),
	)(data)
	return data, f, err
}

type AddRoot struct {
	Name  Atom
	Inode Atom
}

func (a AddRoot) atoms() []Atom {
	return []Atom{a.Name, a.Inode}
}

func addRoot(data []byte) ([]byte, AddRoot, error) {
	var a AddRoot
	data, _, err := tuple(
		assign(&a.Name, atom),
		assign(&a.Inode, atom),
	)(data)
	return data, a, err
}

type Edit struct {
	Change   Atom
	Local    Local
	Encoding string
}

func (e Edit) atoms() []Atom {
	return []Atom{e.Change}
}

func edit(data []byte) ([]byte, Edit, error) {
	var e Edit
	data, _, err := tuple(
		assign(&e.Change, atom),
		assign(&e.Local, local),
		assign(&e.Encoding, mapValue(option(toString(lengthData(uint64LE))), func(p *string) string {
			if p == nil {
				return ""
			}
			return *p
		})),
	)(data)
	return data, e, err
}

func editV4(data []byte) ([]byte, Edit, error) {
	var e Edit
	e.Encoding = "UTF-8"
	data, _, err := tuple(
		assign(&e.Change, atom),
		assign(&e.Local, local),
	)(data)
	return data, e, err
}

type SolveOrderConflict struct {
	Change Atom
	Local  Local
}

func (s SolveOrderConflict) atoms() []Atom {
	return []Atom{s.Change}
}

func solveOrderConflict(data []byte) ([]byte, SolveOrderConflict, error) {
	var s SolveOrderConflict
	data, _, err := tuple(
		assign(&s.Change, atom),
		assign(&s.Local, local),
	)(data)
	return data, s, err
}

type Replacement struct {
	Change      Atom
	Replacement Atom
	Local       Local
	Encoding    string
}

func (r Replacement) atoms() []Atom {
	return []Atom{r.Change, r.Replacement}
}

func replacement(data []byte) ([]byte, Replacement, error) {
	var r Replacement
	data, _, err := tuple(
		assign(&r.Change, atom),
		assign(&r.Replacement, atom),
		assign(&r.Local, local),
		assign(&r.Encoding, mapValue(option(toString(lengthData(uint64LE))), func(p *string) string {
			if p == nil {
				return ""
			}
			return *p
		})),
	)(data)
	return data, r, err
}

func replacementV4(data []byte) ([]byte, Replacement, error) {
	var r Replacement
	r.Encoding = "UTF-8"
	data, _, err := tuple(
		assign(&r.Change, atom),
		assign(&r.Replacement, atom),
		assign(&r.Local, local),
	)(data)
	return data, r, err
}

type FileDel struct {
	Del      Atom
	Contents Atom
	Path     string
	Encoding string
}

func (f FileDel) atoms() []Atom {
	if f.Contents == nil {
		return []Atom{f.Del}
	}
	return []Atom{f.Del, f.Contents}
}

func fileDel(data []byte) ([]byte, FileDel, error) {
	var f FileDel
	data, _, err := tuple(
		assign(&f.Del, atom),
		assign(&f.Contents, mapValue(option(atom), func(p *Atom) Atom {
			if p == nil {
				return nil
			}
			return *p
		})),
		assign(&f.Path, toString(lengthData(uint64LE))),
		assign(&f.Encoding, mapValue(option(toString(lengthData(uint64LE))), func(p *string) string {
			if p == nil {
				return ""
			}
			return *p
		})),
	)(data)
	return data, f, err
}

func fileDelV4(data []byte) ([]byte, FileDel, error) {
	var f FileDel
	f.Encoding = "UTF-8"
	data, _, err := tuple(
		assign(&f.Del, atom),
		assign(&f.Contents, mapValue(option(atom), func(p *Atom) Atom {
			if p == nil {
				return nil
			}
			return *p
		})),
		assign(&f.Path, toString(lengthData(uint64LE))),
	)(data)
	return data, f, err
}

type ResurrectZombies struct {
	Change   Atom
	Local    Local
	Encoding string
}

func (r ResurrectZombies) atoms() []Atom {
	return []Atom{r.Change}
}

func resurrectZombies(data []byte) ([]byte, ResurrectZombies, error) {
	var r ResurrectZombies
	data, _, err := tuple(
		assign(&r.Change, atom),
		assign(&r.Local, local),
		assign(&r.Encoding, optionalString),
	)(data)
	return data, r, err
}

func resurrectZombiesV4(data []byte) ([]byte, ResurrectZombies, error) {
	var r ResurrectZombies
	r.Encoding = "UTF-8"
	data, _, err := tuple(
		assign(&r.Change, atom),
		assign(&r.Local, local),
	)(data)
	return data, r, err
}

type FileUndel struct {
	Undel    Atom
	Contents Atom
	Path     string
	Encoding string
}

func (f FileUndel) atoms() []Atom {
	if f.Contents == nil {
		return []Atom{f.Undel}
	}
	return []Atom{f.Undel, f.Contents}
}

func fileUndel(data []byte) ([]byte, FileUndel, error) {
	var f FileUndel
	data, _, err := tuple(
		assign(&f.Undel, atom),
		assign(&f.Contents, mapValue(option(atom), func(p *Atom) Atom {
			if p == nil {
				return nil
			}
			return *p
		})),
		assign(&f.Path, toString(lengthData(uint64LE))),
		assign(&f.Encoding, optionalString),
	)(data)
	return data, f, err
}

func fileUndelV4(data []byte) ([]byte, FileUndel, error) {
	var f FileUndel
	f.Encoding = "UTF-8"
	data, _, err := tuple(
		assign(&f.Undel, atom),
		assign(&f.Contents, mapValue(option(atom), func(p *Atom) Atom {
			if p == nil {
				return nil
			}
			return *p
		})),
		assign(&f.Path, toString(lengthData(uint64LE))),
	)(data)
	return data, f, err
}

type FileMove struct {
	Del  Atom
	Add  Atom
	Path string
}

func (f FileMove) atoms() []Atom {
	return []Atom{f.Del, f.Add}
}

func fileMove(data []byte) ([]byte, FileMove, error) {
	var f FileMove
	data, _, err := tuple(
		assign(&f.Del, atom),
		assign(&f.Add, atom),
		assign(&f.Path, rustString),
	)(data)
	return data, f, err
}

type SolveNameConflict struct {
	Name Atom
	Path string
}

func (s SolveNameConflict) atoms() []Atom {
	return []Atom{s.Name}
}

func solveNameConflict(data []byte) ([]byte, SolveNameConflict, error) {
	var s SolveNameConflict
	data, _, err := tuple(
		assign(&s.Name, atom),
		assign(&s.Path, rustString),
	)(data)
	return data, s, err
}