/*
 * EliasDB
 *
 * Copyright 2016 Matthias Ladkau. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

package storage

import (
	"bytes"
	"fmt"
	"sync"

	"devt.de/krotik/common/datautil"
	"devt.de/krotik/eliasdb/storage/file"
)

/*
Special flags which cause the manager to return errors on specific function calls
*/
const (
	AccessNotInCache                = 1 // The address will not be accessible via FetchCached
	AccessFetchError                = 2 // The address will not be accessible via Fetch
	AccessUpdateError               = 3 // The address will not be accessible via Update
	AccessFreeError                 = 4 // The address will not be accessible via Free
	AccessInsertError               = 5 // The address will not be accessible via Insert
	AccessCacheAndFetchError        = 6 // The address will not be accessible via FetchCached nor Fetch
	AccessCacheAndFetchSeriousError = 7 // The address will not be accessible via FetchCached nor Fetch
)

/*
MsmRetClose nil or the error which should be returned by a Close call
*/
var MsmRetClose error

/*
MsmCallNumClose counter how often Close is called
*/
var MsmCallNumClose int

/*
MsmRetFlush nil or the error which should be returned by a Flush call
*/
var MsmRetFlush error

/*
MsmCallNumFlush counter how often Flush is called
*/
var MsmCallNumFlush int

/*
MsmRetRollback nil or the error which should be returned by a Rollback call
*/
var MsmRetRollback error

/*
MsmCallNumRollback counter how often Rollback is called
*/
var MsmCallNumRollback int

/*
MemoryStorageManager data structure
*/
type MemoryStorageManager struct {
	name  string                 // Name of the storage manager
	Roots map[int]uint64         // Map of roots
	Data  map[uint64]interface{} // Map of data
	mutex *sync.Mutex            // Mutex to protect map operations

	LocCount  uint64         // Counter for locations - Must start > 0
	AccessMap map[uint64]int // Special map to simulate access issues
}

/*
NewMemoryStorageManager creates a new MemoryStorageManager
*/
func NewMemoryStorageManager(name string) *MemoryStorageManager {

	// LocCount must start > 0 - so they can be stored in a Root. Roots with
	// the value 0 are considered empty. See also graph.getHTree which will
	// keep creating new HTrees if a Root is 0.

	return &MemoryStorageManager{name, make(map[int]uint64),
		make(map[uint64]interface{}), &sync.Mutex{}, 1, make(map[uint64]int)}
}

/*
Name returns the name of the StorageManager instance.
*/
func (msm *MemoryStorageManager) Name() string {
	return msm.name
}

/*
Root returns a root value. Default (empty) value is 0.
*/
func (msm *MemoryStorageManager) Root(root int) uint64 {
	msm.mutex.Lock()
	defer msm.mutex.Unlock()

	return msm.Roots[root]
}

/*
SetRoot writes a root value.
*/
func (msm *MemoryStorageManager) SetRoot(root int, val uint64) {
	msm.mutex.Lock()
	defer msm.mutex.Unlock()

	msm.Roots[root] = val
}

/*
Insert inserts an object and return its storage location.
*/
func (msm *MemoryStorageManager) Insert(o interface{}) (uint64, error) {
	msm.mutex.Lock()
	defer msm.mutex.Unlock()

	if msm.AccessMap[msm.LocCount] == AccessInsertError {
		return 0, file.NewStorageFileError(file.ErrAlreadyInUse, "", "<memory>")
	}
	loc := msm.LocCount
	msm.LocCount++
	msm.Data[loc] = o
	return loc, nil
}

/*
Update updates a storage location.
*/
func (msm *MemoryStorageManager) Update(loc uint64, o interface{}) error {
	msm.mutex.Lock()
	defer msm.mutex.Unlock()

	if msm.AccessMap[loc] == AccessUpdateError {
		return NewStorageManagerError(ErrSlotNotFound, fmt.Sprint("Location:", loc), msm.Name())
	}
	msm.Data[loc] = o
	return nil
}

/*
Free frees a storage location.
*/
func (msm *MemoryStorageManager) Free(loc uint64) error {
	msm.mutex.Lock()
	defer msm.mutex.Unlock()

	if msm.AccessMap[loc] == AccessFreeError {
		return NewStorageManagerError(ErrSlotNotFound, fmt.Sprint("Location:", loc), msm.Name())
	}
	delete(msm.Data, loc)
	return nil
}

/*
Fetch fetches an object from a given storage location and writes it to
a given data container.
*/
func (msm *MemoryStorageManager) Fetch(loc uint64, o interface{}) error {
	var err error

	msm.mutex.Lock()
	defer msm.mutex.Unlock()

	if msm.AccessMap[loc] == AccessFetchError || msm.AccessMap[loc] == AccessCacheAndFetchError {
		return NewStorageManagerError(ErrSlotNotFound, fmt.Sprint("Location:", loc), msm.Name())
	} else if msm.AccessMap[loc] == AccessCacheAndFetchSeriousError {
		return file.NewStorageFileError(file.ErrAlreadyInUse, "", "<memory>")
	}

	if obj, ok := msm.Data[loc]; ok {
		err = datautil.CopyObject(obj, o)
	} else {
		err = NewStorageManagerError(ErrSlotNotFound, fmt.Sprint("Location:", loc), msm.Name())
	}
	return err
}

/*
FetchCached fetches an object from a cache and returns its reference.
Returns a storage.ErrNotInCache error if the entry is not in the cache.
*/
func (msm *MemoryStorageManager) FetchCached(loc uint64) (interface{}, error) {
	msm.mutex.Lock()
	defer msm.mutex.Unlock()

	if msm.AccessMap[loc] == AccessNotInCache || msm.AccessMap[loc] == AccessCacheAndFetchError {
		return nil, NewStorageManagerError(ErrNotInCache, "", msm.Name())
	} else if msm.AccessMap[loc] == AccessCacheAndFetchSeriousError {
		return nil, file.NewStorageFileError(file.ErrAlreadyInUse, "", "<memory>")
	}
	return msm.Data[loc], nil
}

/*
Flush writes all pending changes to disk.
*/
func (msm *MemoryStorageManager) Flush() error {
	MsmCallNumFlush++
	return MsmRetFlush
}

/*
Rollback cancels all pending changes which have not yet been written to disk.
*/
func (msm *MemoryStorageManager) Rollback() error {
	MsmCallNumRollback++
	return MsmRetRollback
}

/*
Close the StorageManager and write all pending changes to disk.
*/
func (msm *MemoryStorageManager) Close() error {
	MsmCallNumClose++
	return MsmRetClose
}

/*
Show a string representation of the storage manager.
*/
func (msm *MemoryStorageManager) String() string {
	buf := new(bytes.Buffer)

	buf.WriteString(fmt.Sprintf("MemoryStorageManager %v\n", msm.name))

	for k, v := range msm.Data {
		buf.WriteString(fmt.Sprintf("%v - %v\n", k, v))
	}

	return buf.String()
}
