//go:generate go run ./gen

package dagspliter

import (
	"context"
	"fmt"
	"os"

	"github.com/docker/go-units"
	"github.com/ipfs/go-cid"
	ipld "github.com/ipfs/go-ipld-format"
	mdag "github.com/ipfs/go-merkledag"
	"github.com/ipfs/go-unixfs"
	uio "github.com/ipfs/go-unixfs/io"
	"golang.org/x/xerrors"
)

/// Box is a way of packing together *partial* DAGs to achieve a certain size
/// while generating the associated CAR file containing them. It is an
/// alternative to actual re-chunking the DAG nodes which can be expensive for
/// very large DAGs. A *partial* DAG is generated by excluding certain sub-DAGs
/// from it.
type Box struct {
	/// CID of the roots of the *partial* DAGs contained in this Box.
	Roots []cid.Cid
	/// CIDs of the roots of the sub-DAGs excluded from the original DAGs
	/// (delimited by Roots). We don't keep track of which sub-DAG is being
	/// trimmed from which full DAG in Roots, so to obtain the *partial* DAGs
	/// one needs to walk each DAG in Roots checking if any of its links
	/// are contained here.
	External []cid.Cid
}

type Builder struct {
	// Service to fetch the nodes in the DAGs and query its links.
	dagService ipld.DAGService

	// Maximum size allowed for each generated Box.
	boxMaxSize uint64

	// Minimum size of graph chunks to bother packing into boxes
	minSubgraphSize uint64

	// Generated boxes when packing a DAG.
	boxes []*Box
	// Used size of the current box we are packing (last one in the list). Since
	// we only pack one box at a time and don't come back to a box once we're
	// done with it we just track a single value here and not in each box.
	boxUsedSize uint64
}

func NewBuilder(dserv ipld.DAGService, chunksize uint64, minSubgraphSize uint64) *Builder {
	bb := &Builder{
		dagService:      dserv,
		boxMaxSize:      chunksize,
		minSubgraphSize: minSubgraphSize,
		boxes:           make([]*Box, 0),
	}
	bb.newBox()
	return bb
}

func getSingleNodeSize(node ipld.Node) uint64 {
	// FIXME: How to check the size of the parent node without taking into
	//  account the children? The Node interface doesn't seem to account for
	//  that so we are going directly to the Block interface for now.
	//  We can probably get away with not accounting non-file data well, and
	//  just have some % overhead when accounting space (obviously that will
	//  break horribly with small files, but it should be good enough in the
	//  average case).
	return uint64(len(node.RawData()))
}

func (b *Builder) Boxes() []*Box {
	return b.boxes
}

// TODO: handle non-protobuf dags
func (b *Builder) getTreeSize(nd ipld.Node) (uint64, error) {
	switch n := nd.(type) {
	case *mdag.RawNode:
		return uint64(len(n.RawData())), nil

	case *mdag.ProtoNode:
		fsNode, err := unixfs.FSNodeFromBytes(n.Data())
		if err != nil {
			return 0, xerrors.Errorf("loading unixfs node: %w", err)
		}

		switch fsNode.Type() {
		case unixfs.TFile, unixfs.TRaw, unixfs.TDirectory, unixfs.THAMTShard:
			return n.Size()
		case unixfs.TMetadata:
			/*if len(n.Links()) == 0 {
				return nil, xerrors.New("incorrectly formatted metadata object")
			}
			child, err := n.Links()[0].GetNode(ctx, b.dagService)
			if err != nil {
				return nil, err
			}
			childpb, ok := child.(*mdag.ProtoNode)
			if !ok {
				return nil, mdag.ErrNotProtobuf
			}*/

			return 0, xerrors.Errorf("metadata object support todo")
		case unixfs.TSymlink:
			return 0, xerrors.Errorf("symlink object support todo")
		default:
			return 0, unixfs.ErrUnrecognizedType
		}
	default:
		return 0, uio.ErrUnkownNodeType
	}
}

func (b *Builder) Pack(ctx context.Context, root cid.Cid) error {
	stack := []cid.Cid{root}

	packed := cid.NewSet()
	for len(stack) > 0 {
		cur := stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		if packed.Has(cur) {
			// already packed this one in, skip it
			continue
		}

		nd, err := b.dagService.Get(ctx, cur)
		if err != nil {
			return err
		}

		size, err := b.getTreeSize(nd)
		if err != nil {
			return err
		}

		if b.fits(uint64(size)) {
			packed.Add(cur)
			b.packRoot(cur)
			b.addSize(uint64(size))
			continue
		} else if b.fits(uint64(len(nd.RawData()))) {
			// this tree doesnt fit in the box, so lets add the node as 'raw' and recurse
			// TODO: check if its a good candidate for going into its own new box
			packed.Add(cur)
			pref := cur.Prefix()
			pref.Codec = cid.Raw
			pref.Version = 1

			ncid, err := pref.Sum(nd.RawData())
			if err != nil {
				return err
			}

			b.addExternalLink(ncid)
			b.addSize(uint64(len(nd.RawData())))

			//  TODO: this means we traverse them in reverse order, not sure if thats the best way forward
			for _, l := range nd.Links() {
				stack = append(stack, l.Cid)
			}
		} else {
			// need a new box, throw this one back on the stack and move on
			stack = append(stack, cur)
			b.newBox()
		}
	}

	return nil
}

// Get current box we are packing into. By definition now this is always the
// last created box.
func (b *Builder) boxID() int {
	return len(b.boxes) - 1
}

// Get current box we are packing into.
func (b *Builder) box() *Box {
	return b.boxes[b.boxID()]
}

func (b *Builder) newBox() {
	b.boxes = append(b.boxes, new(Box))
	b.boxUsedSize = 0
}

// Remaining size in the current box.
func (b *Builder) boxRemainingSize() int64 {
	return int64(b.boxMaxSize) - int64(b.used())
}

func (b *Builder) used() uint64 {
	return b.boxUsedSize
}

func (b *Builder) print(msg string) {
	status := fmt.Sprintf("[BOX %d] <%s>:",
		b.boxID(), units.BytesSize(float64(b.used())))
	fmt.Fprintf(os.Stderr, "%s %s\n", status, msg)
}

// Check this size fits in the current box.
func (b *Builder) fits(size uint64) bool {
	return int64(size) <= b.boxRemainingSize()
}

func (b *Builder) addSize(size uint64) {
	// FIXME: Maybe assert size (`fits`).
	b.boxUsedSize += size
}

func (b *Builder) packRoot(c cid.Cid) {
	b.box().Roots = append(b.box().Roots, c)
}

func (b *Builder) addExternalLink(node cid.Cid) {
	b.box().External = append(b.box().External, node)
}
