/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package main

import (
	"bytes"
	"errors"
	"fmt"
	"github.com/jmhodges/levigo"
	"github.com/ugorji/go/codec"
	"htrace/common"
	"htrace/conf"
	"io"
	"math"
	"math/rand"
	"os"
	"strings"
	"syscall"
	"time"
)

// Routines for loading the datastore.

// The leveldb key which has information about the shard.
const SHARD_INFO_KEY = 'w'

// A constant signifying that we don't know what the layout version is.
const UNKNOWN_LAYOUT_VERSION = 0

// The current layout version.  We cannot read layout versions newer than this.
// We may sometimes be able to read older versions, but only by doing an
// upgrade.
const CURRENT_LAYOUT_VERSION = 3

type DataStoreLoader struct {
	// The dataStore logger.
	lg *common.Logger

	// True if we should clear the stored data.
	ClearStored bool

	// The shards that we're loading
	shards []*ShardLoader

	// The options to use for opening datastores in LevelDB.
	openOpts *levigo.Options

	// The read options to use for LevelDB.
	readOpts *levigo.ReadOptions

	// The write options to use for LevelDB.
	writeOpts *levigo.WriteOptions
}

// Information about a Shard.
type ShardInfo struct {
	// The layout version of the datastore.
	// We should always keep this field so that old software can recognize new
	// layout versions, even if it can't read them.
	LayoutVersion uint64

	// A random number identifying this daemon.
	DaemonId uint64

	// The total number of shards in this datastore.
	TotalShards uint32

	// The index of this shard within the datastore.
	ShardIndex uint32
}

// Create a new datastore loader.
// Initializes the loader, but does not load any leveldb instances.
func NewDataStoreLoader(cnf *conf.Config) *DataStoreLoader {
	dld := &DataStoreLoader{
		lg:          common.NewLogger("datastore", cnf),
		ClearStored: cnf.GetBool(conf.HTRACE_DATA_STORE_CLEAR),
	}
	dld.readOpts = levigo.NewReadOptions()
	dld.readOpts.SetFillCache(true)
	dld.readOpts.SetVerifyChecksums(false)
	dld.writeOpts = levigo.NewWriteOptions()
	dld.writeOpts.SetSync(false)
	dirsStr := cnf.Get(conf.HTRACE_DATA_STORE_DIRECTORIES)
	rdirs := strings.Split(dirsStr, conf.PATH_LIST_SEP)
	// Filter out empty entries
	dirs := make([]string, 0, len(rdirs))
	for i := range rdirs {
		if strings.TrimSpace(rdirs[i]) != "" {
			dirs = append(dirs, rdirs[i])
		}
	}
	dld.shards = make([]*ShardLoader, len(dirs))
	for i := range dirs {
		dld.shards[i] = &ShardLoader{
			dld:  dld,
			path: dirs[i] + conf.PATH_SEP + "db",
		}
	}
	dld.openOpts = levigo.NewOptions()
	cacheSize := cnf.GetInt(conf.HTRACE_LEVELDB_CACHE_SIZE)
	dld.openOpts.SetCache(levigo.NewLRUCache(cacheSize))
	dld.openOpts.SetParanoidChecks(false)
	writeBufferSize := cnf.GetInt(conf.HTRACE_LEVELDB_WRITE_BUFFER_SIZE)
	if writeBufferSize > 0 {
		dld.openOpts.SetWriteBufferSize(writeBufferSize)
	}
	maxFdPerShard := dld.calculateMaxOpenFilesPerShard()
	if maxFdPerShard > 0 {
		dld.openOpts.SetMaxOpenFiles(maxFdPerShard)
	}
	return dld
}

func (dld *DataStoreLoader) Close() {
	if dld.lg != nil {
		dld.lg.Close()
		dld.lg = nil
	}
	if dld.openOpts != nil {
		dld.openOpts.Close()
		dld.openOpts = nil
	}
	if dld.readOpts != nil {
		dld.readOpts.Close()
		dld.readOpts = nil
	}
	if dld.writeOpts != nil {
		dld.writeOpts.Close()
		dld.writeOpts = nil
	}
	if dld.shards != nil {
		for i := range dld.shards {
			if dld.shards[i] != nil {
				dld.shards[i].Close()
			}
		}
		dld.shards = nil
	}
}

func (dld *DataStoreLoader) DisownResources() {
	dld.lg = nil
	dld.openOpts = nil
	dld.readOpts = nil
	dld.writeOpts = nil
	dld.shards = nil
}

// The maximum number of file descriptors we'll use on non-datastore things.
const NON_DATASTORE_FD_MAX = 300

// The minimum number of file descriptors per shard we will set.  Setting fewer
// than this number could trigger a bug in some early versions of leveldb.
const MIN_FDS_PER_SHARD = 80

func (dld *DataStoreLoader) calculateMaxOpenFilesPerShard() int {
	var rlim syscall.Rlimit
	err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim)
	if err != nil {
		dld.lg.Warnf("Unable to calculate maximum open files per shard: "+
			"getrlimit failed: %s\n", err.Error())
		return 0
	}
	// I think RLIMIT_NOFILE fits in 32 bits on all known operating systems,
	// but there's no harm in being careful.  'int' in golang always holds at
	// least 32 bits.
	var maxFd int
	if rlim.Cur > uint64(math.MaxInt32) {
		maxFd = math.MaxInt32
	} else {
		maxFd = int(rlim.Cur)
	}
	if len(dld.shards) == 0 {
		dld.lg.Warnf("Unable to calculate maximum open files per shard, " +
			"since there are 0 shards configured.\n")
		return 0
	}
	fdsPerShard := (maxFd - NON_DATASTORE_FD_MAX) / len(dld.shards)
	if fdsPerShard < MIN_FDS_PER_SHARD {
		dld.lg.Warnf("Expected to be able to use at least %d "+
			"fds per shard, but we have %d shards and %d total fds to allocate, "+
			"giving us only %d FDs per shard.", MIN_FDS_PER_SHARD,
			len(dld.shards), maxFd-NON_DATASTORE_FD_MAX, fdsPerShard)
		return 0
	}
	dld.lg.Infof("maxFd = %d.  Setting maxFdPerShard = %d\n",
		maxFd, fdsPerShard)
	return fdsPerShard
}

// Load information about all shards.
func (dld *DataStoreLoader) LoadShards() {
	for i := range dld.shards {
		shd := dld.shards[i]
		shd.load()
	}
}

// Verify that the shard infos are consistent.
// Reorders the shardInfo structures based on their ShardIndex.
func (dld *DataStoreLoader) VerifyShardInfos() error {
	if len(dld.shards) < 1 {
		return errors.New("No shard directories found.")
	}
	// Make sure no shards had errors.
	for i := range dld.shards {
		shd := dld.shards[i]
		if shd.infoErr != nil {
			return shd.infoErr
		}
	}
	// Make sure that if any shards are empty, all shards are empty.
	emptyShards := ""
	prefix := ""
	for i := range dld.shards {
		if dld.shards[i].info == nil {
			emptyShards = prefix + dld.shards[i].path
			prefix = ", "
		}
	}
	if emptyShards != "" {
		for i := range dld.shards {
			if dld.shards[i].info != nil {
				return errors.New(fmt.Sprintf("Shards %s were empty, but "+
					"the other shards had data.", emptyShards))
			}
		}
		// All shards are empty.
		return nil
	}
	// Make sure that all shards have the same layout version, daemonId, and number of total
	// shards.
	layoutVersion := dld.shards[0].info.LayoutVersion
	daemonId := dld.shards[0].info.DaemonId
	totalShards := dld.shards[0].info.TotalShards
	for i := 1; i < len(dld.shards); i++ {
		shd := dld.shards[i]
		if layoutVersion != shd.info.LayoutVersion {
			return errors.New(fmt.Sprintf("Layout version mismatch.  Shard "+
				"%s has layout version 0x%016x, but shard %s has layout "+
				"version 0x%016x.",
				dld.shards[0].path, layoutVersion, shd.path, shd.info.LayoutVersion))
		}
		if daemonId != shd.info.DaemonId {
			return errors.New(fmt.Sprintf("DaemonId mismatch. Shard %s has "+
				"daemonId 0x%016x, but shard %s has daemonId 0x%016x.",
				dld.shards[0].path, daemonId, shd.path, shd.info.DaemonId))
		}
		if totalShards != shd.info.TotalShards {
			return errors.New(fmt.Sprintf("TotalShards mismatch.  Shard %s has "+
				"TotalShards = %d, but shard %s has TotalShards = %d.",
				dld.shards[0].path, totalShards, shd.path, shd.info.TotalShards))
		}
		if shd.info.ShardIndex >= totalShards {
			return errors.New(fmt.Sprintf("Invalid ShardIndex.  Shard %s has "+
				"ShardIndex = %d, but TotalShards = %d.",
				shd.path, shd.info.ShardIndex, shd.info.TotalShards))
		}
	}
	if layoutVersion != CURRENT_LAYOUT_VERSION {
		return errors.New(fmt.Sprintf("The layout version of all shards "+
			"is %d, but we only support version %d.",
			layoutVersion, CURRENT_LAYOUT_VERSION))
	}
	if totalShards != uint32(len(dld.shards)) {
		return errors.New(fmt.Sprintf("The TotalShards field of all shards "+
			"is %d, but we have %d shards.", totalShards, len(dld.shards)))
	}
	// Reorder shards in order of their ShardIndex.
	reorderedShards := make([]*ShardLoader, len(dld.shards))
	for i := 0; i < len(dld.shards); i++ {
		shd := dld.shards[i]
		shardIdx := shd.info.ShardIndex
		if reorderedShards[shardIdx] != nil {
			return errors.New(fmt.Sprintf("Both shard %s and "+
				"shard %s have ShardIndex %d.", shd.path,
				reorderedShards[shardIdx].path, shardIdx))
		}
		reorderedShards[shardIdx] = shd
	}
	dld.shards = reorderedShards
	return nil
}

func (dld *DataStoreLoader) Load() error {
	var err error
	// If data.store.clear was set, clear existing data.
	if dld.ClearStored {
		err = dld.clearStored()
		if err != nil {
			return err
		}
	}
	// Make sure the shard directories exist in all cases, with a mkdir -p
	for i := range dld.shards {
		err := os.MkdirAll(dld.shards[i].path, 0777)
		if err != nil {
			return errors.New(fmt.Sprintf("Failed to MkdirAll(%s): %s",
				dld.shards[i].path, err.Error()))
		}
	}
	// Get information about each shard, and verify them.
	dld.LoadShards()
	err = dld.VerifyShardInfos()
	if err != nil {
		return err
	}
	if dld.shards[0].ldb != nil {
		dld.lg.Infof("Loaded %d leveldb instances with "+
			"DaemonId of 0x%016x\n", len(dld.shards),
			dld.shards[0].info.DaemonId)
	} else {
		// Create leveldb instances if needed.
		rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
		daemonId := uint64(rnd.Int63())
		dld.lg.Infof("Initializing %d leveldb instances with a new "+
			"DaemonId of 0x%016x\n", len(dld.shards), daemonId)
		dld.openOpts.SetCreateIfMissing(true)
		for i := range dld.shards {
			shd := dld.shards[i]
			shd.ldb, err = levigo.Open(shd.path, shd.dld.openOpts)
			if err != nil {
				return errors.New(fmt.Sprintf("levigo.Open(%s) failed to "+
					"create the shard: %s", shd.path, err.Error()))
			}
			info := &ShardInfo{
				LayoutVersion: CURRENT_LAYOUT_VERSION,
				DaemonId:      daemonId,
				TotalShards:   uint32(len(dld.shards)),
				ShardIndex:    uint32(i),
			}
			err = shd.writeShardInfo(info)
			if err != nil {
				return errors.New(fmt.Sprintf("levigo.Open(%s) failed to "+
					"write shard info: %s", shd.path, err.Error()))
			}
			dld.lg.Infof("Shard %s initialized with ShardInfo %s \n",
				shd.path, asJson(info))
		}
	}
	return nil
}

func (dld *DataStoreLoader) clearStored() error {
	for i := range dld.shards {
		path := dld.shards[i].path
		fi, err := os.Stat(path)
		if err != nil && !os.IsNotExist(err) {
			dld.lg.Errorf("Failed to stat %s: %s\n", path, err.Error())
			return err
		}
		if fi != nil {
			err = os.RemoveAll(path)
			if err != nil {
				dld.lg.Errorf("Failed to clear existing datastore directory %s: %s\n",
					path, err.Error())
				return err
			}
			dld.lg.Infof("Cleared existing datastore directory %s\n", path)
		}
	}
	return nil
}

type ShardLoader struct {
	// The parent DataStoreLoader
	dld *DataStoreLoader

	// Path to the shard
	path string

	// Leveldb instance of the shard
	ldb *levigo.DB

	// Information about the shard
	info *ShardInfo

	// If non-null, the error we encountered trying to load the shard info.
	infoErr error
}

func (shd *ShardLoader) Close() {
	if shd.ldb != nil {
		shd.ldb.Close()
		shd.ldb = nil
	}
}

// Load information about a particular shard.
func (shd *ShardLoader) load() {
	shd.info = nil
	fi, err := os.Stat(shd.path)
	if err != nil {
		if os.IsNotExist(err) {
			shd.infoErr = nil
			return
		}
		shd.infoErr = errors.New(fmt.Sprintf(
			"stat() error on leveldb directory "+
				"%s: %s", shd.path, err.Error()))
		return
	}
	if !fi.Mode().IsDir() {
		shd.infoErr = errors.New(fmt.Sprintf(
			"stat() error on leveldb directory "+
				"%s: inode is not directory.", shd.path))
		return
	}
	var dbDir *os.File
	dbDir, err = os.Open(shd.path)
	if err != nil {
		shd.infoErr = errors.New(fmt.Sprintf(
			"open() error on leveldb directory "+
				"%s: %s.", shd.path, err.Error()))
		return
	}
	defer func() {
		if dbDir != nil {
			dbDir.Close()
		}
	}()
	_, err = dbDir.Readdirnames(1)
	if err != nil {
		if err == io.EOF {
			// The db directory is empty.
			shd.infoErr = nil
			return
		}
		shd.infoErr = errors.New(fmt.Sprintf(
			"Readdirnames() error on leveldb directory "+
				"%s: %s.", shd.path, err.Error()))
		return
	}
	dbDir.Close()
	dbDir = nil
	shd.ldb, err = levigo.Open(shd.path, shd.dld.openOpts)
	if err != nil {
		shd.ldb = nil
		shd.infoErr = errors.New(fmt.Sprintf(
			"levigo.Open() error on leveldb directory "+
				"%s: %s.", shd.path, err.Error()))
		return
	}
	shd.info, err = shd.readShardInfo()
	if err != nil {
		shd.infoErr = err
		return
	}
	shd.infoErr = nil
}

func (shd *ShardLoader) readShardInfo() (*ShardInfo, error) {
	buf, err := shd.ldb.Get(shd.dld.readOpts, []byte{SHARD_INFO_KEY})
	if err != nil {
		return nil, errors.New(fmt.Sprintf("readShardInfo(%s): failed to "+
			"read shard info key: %s", shd.path, err.Error()))
	}
	if len(buf) == 0 {
		return nil, errors.New(fmt.Sprintf("readShardInfo(%s): got zero-"+
			"length value for shard info key.", shd.path))
	}
	mh := new(codec.MsgpackHandle)
	mh.WriteExt = true
	r := bytes.NewBuffer(buf)
	decoder := codec.NewDecoder(r, mh)
	shardInfo := &ShardInfo{
		LayoutVersion: UNKNOWN_LAYOUT_VERSION,
	}
	err = decoder.Decode(shardInfo)
	if err != nil {
		return nil, errors.New(fmt.Sprintf("readShardInfo(%s): msgpack "+
			"decoding failed for shard info key: %s", shd.path, err.Error()))
	}
	return shardInfo, nil
}

func (shd *ShardLoader) writeShardInfo(info *ShardInfo) error {
	mh := new(codec.MsgpackHandle)
	mh.WriteExt = true
	w := new(bytes.Buffer)
	enc := codec.NewEncoder(w, mh)
	err := enc.Encode(info)
	if err != nil {
		return errors.New(fmt.Sprintf("msgpack encoding error: %s",
			err.Error()))
	}
	err = shd.ldb.Put(shd.dld.writeOpts, []byte{SHARD_INFO_KEY}, w.Bytes())
	if err != nil {
		return errors.New(fmt.Sprintf("leveldb write error: %s",
			err.Error()))
	}
	return nil
}
