// 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 (
	"bytes"
	"database/sql"
	"github.com/conformal/btcdb"
	"github.com/conformal/btcwire"
	_ "github.com/mattn/go-sqlite3"
)

// insertGenesisBlock inserts the genesis block of the block chain into the
// database.
func insertGenesisBlock(db *sql.DB) error {
	// Encode the genesis block to raw bytes.
	pver := uint32(btcwire.ProtocolVersion)
	var buf bytes.Buffer
	err := btcwire.GenesisBlock.BtcEncode(&buf, pver)
	if err != nil {
		return err
	}

	// Insert the genesis block along with its hash and protocol encoding
	// version.
	sql := blkqueries[blkInsertSha]
	sha := btcwire.GenesisHash
	_, err = db.Exec(sql, sha.Bytes(), pver, buf.Bytes())
	if err != nil {
		return err
	}

	return nil
}

// InsertBlockData stores a block hash and its associated data block with a
// previous sha of `prevSha' and a version of `pver'.
func (db *SqliteDb) InsertBlockData(sha *btcwire.ShaHash, prevSha *btcwire.ShaHash, pver uint32, buf []byte) (blockid int64, err error) {
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	return db.insertBlockData(sha, prevSha, pver, buf)
}

// insertSha stores a block hash and its associated data block with a
// previous sha of `prevSha' and a version of `pver'.
// insertSha shall be called with db lock held
func (db *SqliteDb) insertBlockData(sha *btcwire.ShaHash, prevSha *btcwire.ShaHash, pver uint32, buf []byte) (blockid int64, err error) {
	tx := &db.txState
	if tx.tx == nil {
		err = db.startTx()
		if err != nil {
			return
		}
	}

	var prevOk bool
	var blkid int64

	prevOk = db.blkExistsSha(prevSha) // exists -> ok
	if !prevOk {
		return 0, btcdb.PrevShaMissing
	}

	result, err := db.blkStmts[blkInsertSha].Exec(sha.Bytes(), pver, buf)
	if err != nil {
		return
	}

	blkid, err = result.LastInsertId()
	if err != nil {
		return 0, err
	}
	blkid -= 1 // skew between btc blockid and sql

	// Because we don't know know what the last idx is, we don't
	// cache unless already cached
	if db.lastBlkShaCached == true {
		db.lastBlkSha = *sha
		db.lastBlkIdx++
	}

	bid := tBlockInsertData{*sha, pver, buf}
	tx.txInsertList = append(tx.txInsertList, bid)
	tx.txDataSz += len(buf)

	blockid = blkid
	return
}

// fetchSha returns the datablock and pver for the given ShaHash.
func (db *SqliteDb) fetchSha(sha btcwire.ShaHash) (buf []byte, pver uint32,
	blkid int64, err error) {

	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	row := db.blkStmts[blkFetchSha].QueryRow(sha.Bytes())

	var blockidx int64
	var databytes []byte
	err = row.Scan(&pver, &databytes, &blockidx)
	if err == sql.ErrNoRows {
		return // no warning
	}
	if err != nil {
		log.Warnf("fail 2 %v", err)
		return
	}
	buf = databytes
	blkid = blockidx - 1 // skew between btc blockid and sql
	return
}

// ExistsSha looks up the given block hash
// returns true if it is present in the database.
func (db *SqliteDb) ExistsSha(sha *btcwire.ShaHash) (exists bool) {
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	_, exists = db.fetchBlockCache(sha)
	if exists {
		return
	}

	// not in cache, try database
	exists = db.blkExistsSha(sha)
	return
}

// blkExistsSha looks up the given block hash
// returns true if it is present in the database.
// CALLED WITH LOCK HELD
func (db *SqliteDb) blkExistsSha(sha *btcwire.ShaHash) bool {
	var pver uint32

	row := db.blkStmts[blkExistsSha].QueryRow(sha.Bytes())
	err := row.Scan(&pver)

	if err == sql.ErrNoRows {
		return false
	}

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

// FetchBlockShaByIdx returns a block sha based on its height in the blockchain.
func (db *SqliteDb) FetchBlockShaByIdx(blkid int64) (sha *btcwire.ShaHash, err error) {
	var row *sql.Row
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	blockidx := blkid + 1 // skew between btc blockid and sql

	row = db.blkStmts[blkFetchIdx].QueryRow(blockidx)

	var shabytes []byte
	err = row.Scan(&shabytes)
	if err != nil {
		return
	}
	var shaval btcwire.ShaHash
	shaval.SetBytes(shabytes)
	return &shaval, nil
}

// FetchIdxRange looks up a range of block by the start and ending ids.
// Fetch is inclusive of the start id and exclusive of the ending id. If the
// special id `AllShas' is provided as endid then FetchIdxRange will fetch all
// shas from startid until no more shas are present.
func (db *SqliteDb) FetchIdxRange(startid, endid int64) (rshalist []btcwire.ShaHash, err error) {
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	startidx := startid + 1 // skew between btc blockid and sql

	var endidx int64
	if endid == btcdb.AllShas {
		endidx = btcdb.AllShas // no skew if asking for all
	} else {
		endidx = endid + 1 // skew between btc blockid and sql
	}
	rows, err := db.blkStmts[blkFetchIdxList].Query(startidx, endidx)
	if err != nil {
		log.Warnf("query failed %v", err)
		return
	}

	var shalist []btcwire.ShaHash
	for rows.Next() {
		var sha btcwire.ShaHash
		var shabytes []byte
		err = rows.Scan(&shabytes)
		if err != nil {
			log.Warnf("wtf? %v", err)
			break
		}
		sha.SetBytes(shabytes)
		shalist = append(shalist, sha)
	}
	rows.Close()
	if err == nil {
		rshalist = shalist
	}
	log.Tracef("FetchIdxRange idx %v %v returned %v shas err %v", startid, endid, len(shalist), err)
	return
}

// NewestSha provides an interface to quickly look up the sha of
// the most recent (end) of the block chain.
func (db *SqliteDb) NewestSha() (sha *btcwire.ShaHash, blkid int64, err error) {
	var row *sql.Row
	var blockidx int64
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	// answer may be cached
	if db.lastBlkShaCached == true {
		shacopy := db.lastBlkSha
		sha = &shacopy
		blkid = db.lastBlkIdx - 1 // skew between btc blockid and sql
		return
	}

	querystr := "SELECT key, blockid FROM block ORDER BY blockid DESC;"

	tx := &db.txState
	if tx.tx != nil {
		row = tx.tx.QueryRow(querystr)
	} else {
		row = db.sqldb.QueryRow(querystr)
	}

	var shabytes []byte
	err = row.Scan(&shabytes, &blockidx)
	if err == nil {
		var retsha btcwire.ShaHash
		retsha.SetBytes(shabytes)
		sha = &retsha
		blkid = blockidx - 1 // skew between btc blockid and sql

		db.lastBlkSha = retsha
		db.lastBlkIdx = blockidx
		db.lastBlkShaCached = true
	}
	return
}

type SqliteBlockIterator struct {
	rows *sql.Rows
	stmt *sql.Stmt
	db   *SqliteDb
}

// NextRow iterates thru all blocks in database.
func (bi *SqliteBlockIterator) NextRow() bool {
	return bi.rows.Next()
}

// Row returns row data for block iterator.
func (bi *SqliteBlockIterator) Row() (key *btcwire.ShaHash, pver uint32,
	buf []byte, err error) {
	var keybytes []byte

	err = bi.rows.Scan(&keybytes, &pver, &buf)
	if err == nil {
		var retkey btcwire.ShaHash
		retkey.SetBytes(keybytes)
		key = &retkey
	}
	return
}

// Close shuts down the iterator when done walking blocks in the database.
func (bi *SqliteBlockIterator) Close() {
	bi.rows.Close()
	bi.stmt.Close()
}

// NewIterateBlocks prepares iterator for all blocks in database.
func (db *SqliteDb) NewIterateBlocks() (btcdb.BlockIterator, error) {
	var bi SqliteBlockIterator
	db.dbLock.Lock()
	defer db.dbLock.Unlock()

	stmt, err := db.sqldb.Prepare("SELECT key, pver, data FROM block ORDER BY blockid;")
	if err != nil {
		return nil, err
	}
	tx := &db.txState
	if tx.tx != nil {
		txstmt := tx.tx.Stmt(stmt)
		stmt.Close()
		stmt = txstmt
	}
	bi.stmt = stmt

	bi.rows, err = bi.stmt.Query()
	if err != nil {
		return nil, err
	}
	bi.db = db

	return &bi, nil
}