//go:build !386 && !arm && !mips && !mipsle
// +build !386,!arm,!mips,!mipsle

/***********************************************************************************************************************
 *
 *   MIT License
 *
 *   Copyright (c) 2021 Nicolas JUHEL
 *
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in all
 *   copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *   SOFTWARE.
 *
 *
 **********************************************************************************************************************/

package nutsdb

import (
	"context"
	"errors"
	"io"
	"strings"
	"sync"
	"sync/atomic"

	liblog "github.com/nabbar/golib/logger"

	dgbstm "github.com/lni/dragonboat/v3/statemachine"
	liberr "github.com/nabbar/golib/errors"
	"github.com/nutsdb/nutsdb"
)

const (
	_RaftBucket          = "_raft"
	_RaftKeyAppliedIndex = "_raft_applied_index"
	_WordByteTrue        = 0xff
)

func newNode(node uint64, cluster uint64, opt Options, fct func(state bool)) dgbstm.IOnDiskStateMachine {
	if fct == nil {
		fct = func(state bool) {}
	}

	o := new(atomic.Value)
	o.Store(opt)

	return &nutsNode{
		n: node,
		c: cluster,
		o: o,
		r: fct,
		d: new(atomic.Value),
	}
}

type nutsNode struct {
	m sync.Mutex       // mutex for struct var
	n uint64           // nodeId
	c uint64           // clusterId
	r func(state bool) // is running
	o *atomic.Value    // options nutsDB
	d *atomic.Value    // nutsDB database pointer
	l liblog.FuncLog   // logger
}

func (n *nutsNode) SetLogger(l liblog.FuncLog) {
	n.m.Lock()
	defer n.m.Unlock()

	if l != nil {
		n.l = l
	}
}

func (n *nutsNode) GetLogger() liblog.Logger {
	n.m.Lock()
	defer n.m.Unlock()

	if n.l != nil {
		return n.l()
	}

	var l = liblog.New(context.Background)
	n.l = func() liblog.Logger {
		return l
	}

	return l
}

func (n *nutsNode) setRunning(state bool) {
	n.m.Lock()
	defer n.m.Unlock()

	if n != nil && n.r != nil {
		n.r(state)
	}
}

func (n *nutsNode) newTx(writable bool) (*nutsdb.Tx, liberr.Error) {
	if db := n.getDb(); db == nil {
		return nil, ErrorDatabaseClosed.Error(nil)
	} else if tx, e := db.Begin(writable); e != nil {
		return nil, ErrorTransactionInit.Error(e)
	} else {
		return tx, nil
	}
}

func (n *nutsNode) getRaftLogIndexLastApplied() (idxRaftlog uint64, err error) {
	var (
		tx *nutsdb.Tx
		en *nutsdb.Entry
	)

	if tx, err = n.newTx(false); err != nil {
		return 0, err
	}

	defer func() {
		_ = tx.Rollback()
	}()

	if en, err = tx.Get(_RaftBucket, []byte(_RaftKeyAppliedIndex)); err != nil && errors.Is(err, nutsdb.ErrBucketNotFound) {
		return 0, nil
	} else if err != nil && errors.Is(err, nutsdb.ErrBucketEmpty) {
		return 0, nil
	} else if err != nil && errors.Is(err, nutsdb.ErrNotFoundKey) {
		return 0, nil
	} else if err != nil && errors.Is(err, nutsdb.ErrKeyNotFound) {
		return 0, nil
	} else if err != nil && errors.Is(err, nutsdb.ErrKeyEmpty) {
		return 0, nil
	} else if err != nil && strings.HasPrefix(err.Error(), "not found bucket") {
		return 0, nil
	} else if err != nil {
		return 0, err
	} else if en.IsZero() {
		return 0, nil
	} else {
		return n.btoi64(en.Value), nil
	}
}

func (n *nutsNode) i64tob(val uint64) []byte {
	r := make([]byte, 8)
	for i := uint64(0); i < 8; i++ {
		r[i] = byte((val >> (i * 8)) & _WordByteTrue)
	}
	return r
}

func (n *nutsNode) btoi64(val []byte) uint64 {
	r := uint64(0)
	for i := uint64(0); i < 8; i++ {
		r |= uint64(val[i]) << (8 * i)
	}
	return r
}

func (n *nutsNode) applyRaftLogIndexLastApplied(idx uint64) error {
	var (
		e   error
		tx  *nutsdb.Tx
		err liberr.Error
	)

	if tx, err = n.newTx(true); err != nil {
		return err
	}

	defer func() {
		_ = tx.Rollback()
	}()

	if e = tx.Put(_RaftBucket, []byte(_RaftKeyAppliedIndex), n.i64tob(idx), 0); e != nil {
		return ErrorTransactionPutKey.Error(e)
	} else if e = tx.Commit(); e != nil {
		return ErrorTransactionCommit.Error(e)
	} else {
		return nil
	}
}

// Open @TODO : analyze channel role !!
func (n *nutsNode) Open(stopc <-chan struct{}) (idxRaftlog uint64, err error) {
	var (
		opt Options
		db  *nutsdb.DB
	)

	if opt = n.getOptions(); opt == nil {
		return 0, ErrorValidateConfig.Error(nil)
	}

	if db, err = nutsdb.Open(opt.NutsDBOptions()); err != nil {
		return 0, err
	} else {
		n.setDb(db)
		n.setRunning(true)

		if idxRaftlog, err = n.getRaftLogIndexLastApplied(); err != nil {
			_ = n.Close()
		}

		return
	}
}

func (n *nutsNode) Close() error {
	defer n.setRunning(false)

	if db := n.getDb(); db != nil {
		err := db.Close()
		n.setDb(db)
		return err
	}

	return nil
}

func (n *nutsNode) Update(logEntry []dgbstm.Entry) ([]dgbstm.Entry, error) {
	var (
		e error

		tx *nutsdb.Tx
		kv *CommandRequest

		err liberr.Error
		res []byte
		idx int
		ent dgbstm.Entry
	)

	if tx, err = n.newTx(true); err != nil {
		return nil, err
	}

	for idx, ent = range logEntry {
		if kv, err = NewCommandByDecode(n.GetLogger, ent.Cmd); err != nil {
			logEntry[idx].Result = dgbstm.Result{
				Value: 0,
				Data:  nil,
			}

			_ = tx.Rollback()
			return logEntry, err
		}

		if res, err = kv.Run(tx); err != nil {
			logEntry[idx].Result = dgbstm.Result{
				Value: 0,
				Data:  nil,
			}

			_ = tx.Rollback()
			return logEntry, err
		} else {
			logEntry[idx].Result = dgbstm.Result{
				Value: uint64(idx),
				Data:  res,
			}
		}
	}

	if e = tx.Commit(); e != nil {
		_ = tx.Rollback()
		return logEntry, ErrorTransactionCommit.Error(e)
	}

	return logEntry, n.applyRaftLogIndexLastApplied(logEntry[len(logEntry)-1].Index)
}

func (n *nutsNode) Lookup(query interface{}) (value interface{}, err error) {
	var (
		t *nutsdb.Tx
		r *CommandResponse
		c *CommandRequest
		e liberr.Error

		ok bool
	)

	if t, e = n.newTx(true); e != nil {
		return nil, e
	}

	defer func() {
		if t != nil {
			_ = t.Rollback()
		}
	}()

	if c, ok = query.(*CommandRequest); !ok {
		return nil, ErrorCommandInvalid.Error(nil)
	} else if r, e = c.RunLocal(t); e != nil {
		return nil, e
	} else {
		return r, nil
	}
}

func (n *nutsNode) Sync() error {
	return nil
}

func (n *nutsNode) PrepareSnapshot() (interface{}, error) {
	var sh = newSnap()

	if opt := n.getOptions(); opt == nil {
		return nil, ErrorValidateConfig.Error(nil)
	} else if db := n.getDb(); db == nil {
		sh.Finish()
		return nil, ErrorDatabaseClosed.Error(nil)
	} else if err := sh.Prepare(opt, db); err != nil {
		sh.Finish()
		return nil, ErrorDatabaseBackup.Error(err)
	} else {
		return sh, nil
	}
}

func (n *nutsNode) SaveSnapshot(i interface{}, writer io.Writer, c <-chan struct{}) error {
	if i == nil {
		return ErrorParamEmpty.Error(nil)
	} else if sh, ok := snapCast(i); !ok {
		return ErrorParamMismatching.Error(nil)
	} else if opt := n.getOptions(); opt == nil {
		return ErrorValidateConfig.Error(nil)
	} else if err := sh.Save(opt, writer); err != nil {
		sh.Finish()
		return err
	} else {
		sh.Finish()
	}

	return nil
}

func (n *nutsNode) RecoverFromSnapshot(reader io.Reader, c <-chan struct{}) error {
	var (
		sh  = newSnap()
		opt = n.getOptions()
	)

	defer sh.Finish()

	if opt == nil {
		return ErrorValidateConfig.Error(nil)
	}

	if err := sh.Load(opt, reader); err != nil {
		return err
	}

	if db := n.getDb(); db != nil {
		_ = db.Close()
		n.setDb(db)
	}

	if err := sh.Apply(opt); err != nil {
		return err
	}

	//@TODO : check channel is ok....
	if _, err := n.Open(nil); err != nil {
		return err
	}

	return nil
}

func (n *nutsNode) getDb() *nutsdb.DB {
	n.m.Lock()
	defer n.m.Unlock()

	if n.d == nil {
		return nil
	} else if i := n.d.Load(); i == nil {
		return nil
	} else if db, ok := i.(*nutsdb.DB); !ok {
		return nil
	} else {
		return db
	}
}

func (n *nutsNode) setDb(db *nutsdb.DB) {
	n.m.Lock()
	defer n.m.Unlock()

	if n.d == nil {
		n.d = new(atomic.Value)
	}

	n.d.Store(db)
}

func (n *nutsNode) getOptions() Options {
	n.m.Lock()
	defer n.m.Unlock()

	if n.o == nil {
		return nil
	} else if i := n.o.Load(); i == nil {
		return nil
	} else if opt, ok := i.(Options); !ok {
		return nil
	} else {
		return opt
	}
}
