package blobovniczatree

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/compression"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"git.frostfs.info/TrueCloudLab/hrw"
)

// Blobovniczas represents the storage of the "small" objects.
//
// Each object is stored in Blobovnicza's (B-s).
// B-s are structured in a multilevel directory hierarchy
// with fixed depth and width (configured by BlobStor).
//
// Example (width = 4, depth = 3):
//
// x===============================x
// |[0]    [1]    [2]    [3]|
// |   \                /   |
// |    \              /    |
// |     \            /     |
// |      \          /      |
// |[0]    [1]    [2]    [3]|
// |        |    /          |
// |        |   /           |
// |        |  /            |
// |        | /             |
// |[0](F) [1](A) [X]    [X]|
// x===============================x
//
// Elements of the deepest level are B-s.
// B-s are allocated dynamically. At each moment of the time there is
// an active B (ex. A), set of already filled B-s (ex. F) and
// a list of not yet initialized B-s (ex. X). After filling the active B
// it becomes full, and next B becomes initialized and active.
//
// Active B and some of the full B-s are cached (LRU). All cached
// B-s are intitialized and opened.
//
// Object is saved as follows:
//  1. at each level, according to HRW, the next one is selected and
//     dives into it until we reach the deepest;
//  2. at the B-s level object is saved to the active B. If active B
//     is full, next B is opened, initialized and cached. If there
//     is no more X candidates, goto 1 and process next level.
//
// After the object is saved in B, path concatenation is returned
// in system path format as B identifier (ex. "0/1/1" or "3/2/1").
type Blobovniczas struct {
	cfg

	commondbManager        *dbManager
	activeDBManager        *activeDBManager
	dbCache                *dbCache
	deleteProtectedObjects *addressMap
	dbFilesGuard           *sync.RWMutex
	rebuildGuard           *sync.RWMutex
}

var _ common.Storage = (*Blobovniczas)(nil)

var errPutFailed = errors.New("could not save the object in any blobovnicza")

const (
	dbExtension = ".db"
)

// NewBlobovniczaTree returns new instance of blobovniczas tree.
func NewBlobovniczaTree(opts ...Option) (blz *Blobovniczas) {
	blz = new(Blobovniczas)
	initConfig(&blz.cfg)

	for i := range opts {
		opts[i](&blz.cfg)
	}

	if blz.blzLeafWidth == 0 {
		blz.blzLeafWidth = blz.blzShallowWidth
	}

	blz.commondbManager = newDBManager(blz.rootPath, blz.blzOpts, blz.readOnly, blz.metrics.Blobovnicza(), blz.log)
	blz.activeDBManager = newActiveDBManager(blz.commondbManager, blz.blzLeafWidth)
	blz.dbCache = newDBCache(blz.openedCacheSize, blz.openedCacheTTL, blz.commondbManager)
	blz.deleteProtectedObjects = newAddressMap()
	blz.dbFilesGuard = &sync.RWMutex{}
	blz.rebuildGuard = &sync.RWMutex{}

	return blz
}

// returns hash of the object address.
func addressHash(addr *oid.Address, path string) uint64 {
	var a string

	if addr != nil {
		a = addr.EncodeToString()
	}

	return hrw.StringHash(a + path)
}

func u64ToHexString(ind uint64) string {
	return strconv.FormatUint(ind, 16)
}

func u64ToHexStringExt(ind uint64) string {
	return strconv.FormatUint(ind, 16) + dbExtension
}

func u64FromHexString(str string) uint64 {
	v, err := strconv.ParseUint(strings.TrimSuffix(str, dbExtension), 16, 64)
	if err != nil {
		panic(fmt.Sprintf("blobovnicza name is not an index %s", str))
	}

	return v
}

// Type is blobovniczatree storage type used in logs and configuration.
const Type = "blobovnicza"

// Type implements common.Storage.
func (b *Blobovniczas) Type() string {
	return Type
}

// Path implements common.Storage.
func (b *Blobovniczas) Path() string {
	return b.rootPath
}

// SetCompressor implements common.Storage.
func (b *Blobovniczas) SetCompressor(cc *compression.Config) {
	b.compression = cc
}

func (b *Blobovniczas) Compressor() *compression.Config {
	return b.compression
}

// SetReportErrorFunc implements common.Storage.
func (b *Blobovniczas) SetReportErrorFunc(f func(string, error)) {
	b.reportError = f
}

func (b *Blobovniczas) SetParentID(parentID string) {
	b.metrics.SetParentID(parentID)
}