// Copyright (c) 2013 Conformal Systems LLC.
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package sqlite3

import (
	"database/sql"
	"github.com/conformal/btcdb"
	"github.com/conformal/btcwire"
	_ "github.com/mattn/go-sqlite3"
)

// InsertTx inserts a tx hash and its associated data into the database.
func (db *SqliteDb) InsertTx(txsha *btcwire.ShaHash, height int64, txoff int, txlen int, usedbuf []byte) (err error) {
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	return db.insertTx(txsha, height, txoff, txlen, usedbuf)
}

// insertTx inserts a tx hash and its associated data into the database.
// Must be called with db lock held.
func (db *SqliteDb) insertTx(txsha *btcwire.ShaHash, height int64, txoff int, txlen int, usedbuf []byte) (err error) {

	tx := &db.txState
	if tx.tx == nil {
		err = db.startTx()
		if err != nil {
			return
		}
	}
	blockid := height + 1
	txd := tTxInsertData{txsha: txsha, blockid: blockid, txoff: txoff, txlen: txlen, usedbuf: usedbuf}

	log.Tracef("inserting tx %v for block %v off %v len %v",
		txsha, blockid, txoff, txlen)

	rowBytes := txsha.String()

	var op int // which table to insert data into.
	if db.UseTempTX {
		var tblockid int64
		var ttxoff int
		var ttxlen int
		txop := db.txop(txFetchLocationByShaStmt)
		row := txop.QueryRow(rowBytes)
		err = row.Scan(&tblockid, &ttxoff, &ttxlen)
		if err != sql.ErrNoRows {
			// sha already present
			err = btcdb.DuplicateSha
			return
		}
		op = txtmpInsertStmt
	} else {
		op = txInsertStmt
	}

	txop := db.txop(op)
	_, err = txop.Exec(rowBytes, blockid, txoff, txlen, usedbuf)
	if err != nil {
		log.Warnf("failed to insert %v %v %v", txsha, blockid, err)
		return
	}
	if db.UseTempTX {
		db.TempTblSz++
	}

	// put in insert list for replay
	tx.txInsertList = append(tx.txInsertList, txd)

	return
}

// ExistsTxSha returns if the given tx sha exists in the database
func (db *SqliteDb) ExistsTxSha(txsha *btcwire.ShaHash) (exists bool) {
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	if _, ok := db.fetchTxCache(txsha); ok {
		return true
	}

	return db.existsTxSha(txsha)
}

// existsTxSha returns if the given tx sha exists in the database.o
// Must be called with the db lock held.
func (db *SqliteDb) existsTxSha(txsha *btcwire.ShaHash) (exists bool) {
	var blockid uint32

	txop := db.txop(txExistsShaStmt)
	row := txop.QueryRow(txsha.String())
	err := row.Scan(&blockid)

	if err == sql.ErrNoRows {
		txop = db.txop(txtmpExistsShaStmt)
		row = txop.QueryRow(txsha.String())
		err := row.Scan(&blockid)

		if err == sql.ErrNoRows {
			return false
		}
		if err != nil {
			log.Warnf("txTmpExistsTxSha: fail %v", err)
			return false
		}
		log.Warnf("txtmpExistsTxSha: success")
		return true
	}

	if err != nil {
		// ignore real errors?
		log.Warnf("existsTxSha: fail %v", err)
		return false
	}

	return true
}

// FetchLocationBySha looks up the Tx sha information by name.
func (db *SqliteDb) FetchLocationBySha(txsha *btcwire.ShaHash) (blockidx int64, txoff int, txlen int, err error) {
	db.dbLock.Lock()
	defer db.dbLock.Unlock()
	return db.fetchLocationBySha(txsha)
}

// fetchLocationBySha look up the Tx sha information by name.
// Must be called with db lock held.
func (db *SqliteDb) fetchLocationBySha(txsha *btcwire.ShaHash) (height int64, txoff int, txlen int, err error) {
	var row *sql.Row
	var blockid int64
	var ttxoff int
	var ttxlen int

	rowBytes := txsha.String()
	txop := db.txop(txFetchLocationByShaStmt)
	row = txop.QueryRow(rowBytes)

	err = row.Scan(&blockid, &ttxoff, &ttxlen)
	if err == sql.ErrNoRows {
		txop = db.txop(txtmpFetchLocationByShaStmt)
		row = txop.QueryRow(rowBytes)

		err = row.Scan(&blockid, &ttxoff, &ttxlen)
		if err == sql.ErrNoRows {
			err = btcdb.TxShaMissing
			return
		}
		if err != nil {
			log.Warnf("txtmp FetchLocationBySha: fail %v",
				err)
			return
		}
	}
	if err != nil {
		log.Warnf("FetchLocationBySha: fail %v", err)
		return
	}
	height = blockid - 1
	txoff = ttxoff
	txlen = ttxlen
	return
}

// fetchLocationUsedBySha look up the Tx sha information by name.
// Must be called with db lock held.
func (db *SqliteDb) fetchLocationUsedBySha(txsha *btcwire.ShaHash) (rheight int64, rtxoff int, rtxlen int, rspentbuf []byte, err error) {
	var row *sql.Row
	var blockid int64
	var txoff int
	var txlen int
	var txspent []byte

	rowBytes := txsha.String()
	txop := db.txop(txFetchLocUsedByShaStmt)
	row = txop.QueryRow(rowBytes)

	err = row.Scan(&blockid, &txoff, &txlen, &txspent)
	if err == sql.ErrNoRows {
		txop = db.txop(txtmpFetchLocUsedByShaStmt)
		row = txop.QueryRow(rowBytes)

		err = row.Scan(&blockid, &txoff, &txlen, &txspent)
		if err == sql.ErrNoRows {
			err = btcdb.TxShaMissing
			return
		}
		if err != nil {
			log.Warnf("txtmp FetchLocationBySha: fail %v",
				err)
			return
		}
	}
	if err != nil {
		log.Warnf("FetchLocationBySha: fail %v", err)
		return
	}
	height := blockid - 1
	return height, txoff, txlen, txspent, nil
}

// FetchTxUsedBySha returns the used/spent buffer for a given transaction.
func (db *SqliteDb) FetchTxUsedBySha(txsha *btcwire.ShaHash) (spentbuf []byte, err error) {
	var row *sql.Row
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	rowBytes := txsha.String()
	txop := db.txop(txFetchUsedByShaStmt)
	row = txop.QueryRow(rowBytes)

	var databytes []byte
	err = row.Scan(&databytes)
	if err == sql.ErrNoRows {
		txop := db.txop(txtmpFetchUsedByShaStmt)
		row = txop.QueryRow(rowBytes)

		err = row.Scan(&databytes)
		if err == sql.ErrNoRows {
			err = btcdb.TxShaMissing
			return
		}
		if err != nil {
			log.Warnf("txtmp FetchLocationBySha: fail %v",
				err)
			return
		}
	}

	if err != nil {
		log.Warnf("FetchUsedBySha: fail %v", err)
		return
	}
	spentbuf = databytes
	return
}

var vaccumDbNextMigrate bool

// migrateTmpTable functions to perform internal db optimization when
// performing large numbers of database inserts. When in Fast operation
// mode, it inserts into txtmp, then when that table reaches a certain
// size limit it moves all tx in the txtmp table into the primary tx
// table and recomputes the index on the primary tx table.
func (db *SqliteDb) migrateTmpTable() error {
	db.endTx(true)
	db.startTx() // ???

	db.UseTempTX = false
	db.TempTblSz = 0

	var doVacuum bool
	var nsteps int
	if vaccumDbNextMigrate {
		nsteps = 6
		vaccumDbNextMigrate = false
		doVacuum = true
	} else {
		nsteps = 5
		vaccumDbNextMigrate = true
	}

	log.Infof("db compaction Stage 1/%v: Preparing", nsteps)
	txop := db.txop(txMigratePrep)
	_, err := txop.Exec()
	if err != nil {
		log.Warnf("Failed to prepare migrate - %v", err)
		return err
	}

	log.Infof("db compaction Stage 2/%v: Copying", nsteps)
	txop = db.txop(txMigrateCopy)
	_, err = txop.Exec()
	if err != nil {
		log.Warnf("Migrate read failed - %v", err)
		return err
	}

	log.Tracef("db compaction Stage 2a/%v: Enable db vacuum", nsteps)
	txop = db.txop(txPragmaVacuumOn)
	_, err = txop.Exec()
	if err != nil {
		log.Warnf("Migrate error trying to enable vacuum on "+
			"temporary transaction table - %v", err)
		return err
	}

	log.Infof("db compaction Stage 3/%v: Clearing old data", nsteps)
	txop = db.txop(txMigrateClear)
	_, err = txop.Exec()
	if err != nil {
		log.Warnf("Migrate error trying to clear temporary "+
			"transaction table - %v", err)
		return err
	}

	log.Tracef("db compaction Stage 3a/%v: Disable db vacuum", nsteps)
	txop = db.txop(txPragmaVacuumOff)
	_, err = txop.Exec()
	if err != nil {
		log.Warnf("Migrate error trying to disable vacuum on "+
			"temporary transaction table - %v", err)
		return err
	}

	log.Infof("db compaction Stage 4/%v: Rebuilding index", nsteps)
	txop = db.txop(txMigrateFinish)
	_, err = txop.Exec()
	if err != nil {
		log.Warnf("Migrate error trying to clear temporary "+
			"transaction table - %v", err)
		return err
	}

	log.Infof("db compaction Stage 5/%v: Finalizing transaction", nsteps)
	db.endTx(true) // ???

	if doVacuum {
		log.Infof("db compaction Stage 6/%v: Optimizing database", nsteps)
		txop = db.txop(txVacuum)
		_, err = txop.Exec()
		if err != nil {
			log.Warnf("migrate error trying to clear txtmp tbl %v", err)
			return err
		}
	}

	log.Infof("db compaction: Complete")

	// TODO(drahn) - determine if this should be turned back on or not
	db.UseTempTX = true

	return nil
}