package cid

import (
	"encoding/binary"
	"fmt"

	mbase "github.com/multiformats/go-multibase"
	mh "github.com/multiformats/go-multihash"
)

//=================
// def & accessors
//=================

var _ Cid = CidStr("")
var _ map[CidStr]struct{} = nil

// CidStr is a representation of a Cid as a string type containing binary.
//
// Using golang's string type is preferable over byte slices even for binary
// data because golang strings are immutable, usable as map keys,
// trivially comparable with built-in equals operators, etc.
//
// Please do not cast strings or bytes into the CidStr type directly;
// use a parse method which validates the data and yields a CidStr.
type CidStr string

// EmptyCidStr is a constant for a zero/uninitialized/sentinelvalue cid;
// it is declared mainly for readability in checks for sentinel values.
const EmptyCidStr = CidStr("")

func (c CidStr) Version() uint64 {
	bytes := []byte(c)
	v, _ := binary.Uvarint(bytes)
	return v
}

func (c CidStr) Multicodec() uint64 {
	bytes := []byte(c)
	_, n := binary.Uvarint(bytes) // skip version length
	codec, _ := binary.Uvarint(bytes[n:])
	return codec
}

func (c CidStr) Multihash() mh.Multihash {
	bytes := []byte(c)
	_, n1 := binary.Uvarint(bytes)      // skip version length
	_, n2 := binary.Uvarint(bytes[n1:]) // skip codec length
	return mh.Multihash(bytes[n1+n2:])  // return slice of remainder
}

// String returns the default string representation of a Cid.
// Currently, Base58 is used as the encoding for the multibase string.
func (c CidStr) String() string {
	switch c.Version() {
	case 0:
		return c.Multihash().B58String()
	case 1:
		mbstr, err := mbase.Encode(mbase.Base58BTC, []byte(c))
		if err != nil {
			panic("should not error with hardcoded mbase: " + err.Error())
		}
		return mbstr
	default:
		panic("not possible to reach this point")
	}
}

// Bytes produces a raw binary format of the CID.
//
// (For CidStr, this method is only distinct from casting because of
// compatibility with v0 CIDs.)
func (c CidStr) Bytes() []byte {
	switch c.Version() {
	case 0:
		return c.Multihash()
	case 1:
		return []byte(c)
	default:
		panic("not possible to reach this point")
	}
}

// Prefix builds and returns a Prefix out of a Cid.
func (c CidStr) Prefix() Prefix {
	dec, _ := mh.Decode(c.Multihash()) // assuming we got a valid multiaddr, this will not error
	return Prefix{
		MhType:   dec.Code,
		MhLength: dec.Length,
		Version:  c.Version(),
		Codec:    c.Multicodec(),
	}
}

//==================================
// parsers & validators & factories
//==================================

func NewCidStr(version uint64, codecType uint64, mhash mh.Multihash) CidStr {
	hashlen := len(mhash)
	// two 8 bytes (max) numbers plus hash
	buf := make([]byte, 2*binary.MaxVarintLen64+hashlen)
	n := binary.PutUvarint(buf, version)
	n += binary.PutUvarint(buf[n:], codecType)
	cn := copy(buf[n:], mhash)
	if cn != hashlen {
		panic("copy hash length is inconsistent")
	}
	return CidStr(buf[:n+hashlen])
}

// CidStrParse takes a binary byte slice, parses it, and returns either
// a valid CidStr, or the zero CidStr and an error.
//
// For CidV1, the data buffer is in the form:
//
//	<version><codec-type><multihash>
//
// CidV0 are also supported. In particular, data buffers starting
// with length 34 bytes, which starts with bytes [18,32...] are considered
// binary multihashes.
//
// The multicodec bytes are not parsed to verify they're a valid varint;
// no further reification is performed.
//
// Multibase encoding should already have been unwrapped before parsing;
// if you have a multibase-enveloped string, use CidStrDecode instead.
//
// CidStrParse is the inverse of Cid.Bytes().
func CidStrParse(data []byte) (CidStr, error) {
	if len(data) == 34 && data[0] == 18 && data[1] == 32 {
		h, err := mh.Cast(data)
		if err != nil {
			return EmptyCidStr, err
		}
		return NewCidStr(0, DagProtobuf, h), nil
	}

	vers, n := binary.Uvarint(data)
	if err := uvError(n); err != nil {
		return EmptyCidStr, err
	}

	if vers != 0 && vers != 1 {
		return EmptyCidStr, fmt.Errorf("invalid cid version number: %d", vers)
	}

	_, cn := binary.Uvarint(data[n:])
	if err := uvError(cn); err != nil {
		return EmptyCidStr, err
	}

	rest := data[n+cn:]
	h, err := mh.Cast(rest)
	if err != nil {
		return EmptyCidStr, err
	}

	// REVIEW: if the data is longer than the mh.len expects, we silently ignore it?  should we?
	return CidStr(data[0 : n+cn+len(h)]), nil
}
