package xorm

import (
	"sync"

	"github.com/xormplus/xorm/internal/utils"
	"github.com/xormplus/xorm/schemas"
)

const (
	PROPAGATION_REQUIRED      = 0 //Support a current transaction; create a new one if none exists.
	PROPAGATION_SUPPORTS      = 1 //Support a current transaction; execute non-transactionally if none exists.
	PROPAGATION_MANDATORY     = 2 //Support a current transaction; return an error if no current transaction exists.
	PROPAGATION_REQUIRES_NEW  = 3 //Create a new transaction, suspending the current transaction if one exists.
	PROPAGATION_NOT_SUPPORTED = 4 //Do not support a current transaction; rather always execute non-transactionally.
	PROPAGATION_NEVER         = 5 //Do not support a current transaction; return an error if a current transaction exists.
	PROPAGATION_NESTED        = 6 //Execute within a nested transaction if a current transaction exists, behave like PROPAGATION_REQUIRED else.
	PROPAGATION_NOT_REQUIRED  = 7
)

type Transaction struct {
	txSession             *Session
	transactionDefinition int
	isNested              bool
	savePointID           string
}

func (transaction *Transaction) TransactionDefinition() int {
	return transaction.transactionDefinition
}

func (transaction *Transaction) IsExistingTransaction() bool {
	if transaction.txSession.tx == nil {
		return false
	} else {
		return true
	}
}

func (transaction *Transaction) GetSavePointID() string {
	return transaction.savePointID
}

func (transaction *Transaction) Session() *Session {
	return transaction.txSession
}

func (transaction *Transaction) Do(doFunc func(params ...interface{}), params ...interface{}) {
	if transaction.isNested {
		go doFunc(params...)
	} else {
		doFunc(params...)
	}
}

func (transaction *Transaction) WaitForDo(doFunc func(params ...interface{}), params ...interface{}) {
	if transaction.isNested {
		var w sync.WaitGroup
		w.Add(1)
		go func() {
			doFunc(params...)
			w.Done()
		}()
		w.Wait()
	} else {
		doFunc(params...)
	}
}

func (session *Session) BeginTrans(transactionDefinition ...int) (*Transaction, error) {
	var tx *Transaction
	if len(transactionDefinition) == 0 {
		tx = session.transaction(PROPAGATION_REQUIRED)
	} else {
		tx = session.transaction(transactionDefinition[0])
	}

	err := tx.BeginTrans()
	if err != nil {
		return nil, err
	}
	return tx, nil
}

func (session *Session) transaction(transactionDefinition int) *Transaction {
	if transactionDefinition > 6 || transactionDefinition < 0 {
		return &Transaction{txSession: session, transactionDefinition: PROPAGATION_REQUIRED}
	}
	return &Transaction{txSession: session, transactionDefinition: transactionDefinition}
}

// Begin a transaction
func (transaction *Transaction) BeginTrans() error {
	switch transaction.transactionDefinition {
	case PROPAGATION_REQUIRED:
		if !transaction.IsExistingTransaction() {
			if err := transaction.txSession.Begin(); err != nil {
				return err
			}
		} else {
			if transaction.txSession.currentTransaction != nil {
				transaction.savePointID = transaction.txSession.currentTransaction.savePointID
			}
			transaction.isNested = true
		}
		transaction.txSession.currentTransaction = transaction
		return nil
	case PROPAGATION_SUPPORTS:
		if transaction.IsExistingTransaction() {
			transaction.isNested = true
			if transaction.txSession.currentTransaction != nil {
				transaction.savePointID = transaction.txSession.currentTransaction.savePointID
			}
			transaction.txSession.currentTransaction = transaction
		}
		return nil
	case PROPAGATION_MANDATORY:
		if !transaction.IsExistingTransaction() {
			return ErrNestedTransaction
		} else {
			if transaction.txSession.currentTransaction != nil {
				transaction.savePointID = transaction.txSession.currentTransaction.savePointID
			}
			transaction.isNested = true
			transaction.txSession.currentTransaction = transaction
		}
		return nil
	case PROPAGATION_REQUIRES_NEW:
		transaction.txSession = transaction.txSession.engine.NewSession()
		if err := transaction.txSession.Begin(); err != nil {
			return err
		}
		transaction.isNested = false
		transaction.txSession.currentTransaction = transaction
		return nil
	case PROPAGATION_NOT_SUPPORTED:
		if transaction.IsExistingTransaction() {
			transaction.isNested = true
			transaction.txSession = transaction.txSession.engine.NewSession()
		}
		return nil
	case PROPAGATION_NEVER:
		if transaction.IsExistingTransaction() {
			return ErrNestedTransaction
		}
		return nil
	case PROPAGATION_NESTED:
		if !transaction.IsExistingTransaction() {
			if err := transaction.txSession.Begin(); err != nil {
				return err
			}
		} else {
			transaction.isNested = true
			dbtype := transaction.txSession.engine.Dialect().URI().DBType
			if dbtype == schemas.MSSQL {
				transaction.savePointID = "xorm" + utils.NewShortUUID().String()
			} else {
				transaction.savePointID = "xorm" + utils.NewV1().WithoutDashString()
			}

			if err := transaction.SavePoint(transaction.savePointID); err != nil {
				return err
			}
			transaction.txSession.isAutoCommit = false
			transaction.txSession.isCommitedOrRollbacked = false
			transaction.txSession.currentTransaction = transaction

		}
		return nil
	case PROPAGATION_NOT_REQUIRED:
		if transaction.IsExistingTransaction() {
			return ErrNestedTransaction
		}

		if err := transaction.txSession.Begin(); err != nil {
			return err
		}
		return nil
	default:
		return ErrTransactionDefinition
	}

}

// Commit When using transaction, Commit will commit all operations.
func (transaction *Transaction) CommitTrans() error {
	switch transaction.transactionDefinition {
	case PROPAGATION_REQUIRED:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}
		if !transaction.isNested {
			err := transaction.txSession.Commit()
			if err != nil {
				return err
			}
		}
		return nil
	case PROPAGATION_SUPPORTS:
		if transaction.IsExistingTransaction() {
			if !transaction.isNested {
				err := transaction.txSession.Commit()
				if err != nil {
					return err
				}
			}
		}
		return nil
	case PROPAGATION_MANDATORY:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}
		if !transaction.isNested {
			err := transaction.txSession.Commit()
			if err != nil {
				return err
			}
		}
		return nil
	case PROPAGATION_REQUIRES_NEW:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}
		if !transaction.isNested {
			err := transaction.txSession.Commit()
			if err != nil {
				return err
			}
		}
		return nil
	case PROPAGATION_NOT_SUPPORTED:
		if transaction.IsExistingTransaction() {
			return ErrNestedTransaction
		}
		return nil
	case PROPAGATION_NEVER:
		if transaction.IsExistingTransaction() {
			return ErrNestedTransaction
		}
		return nil
	case PROPAGATION_NESTED:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}

		if !transaction.isNested {
			err := transaction.txSession.Commit()
			if err != nil {
				return err
			}
		} else if transaction.txSession.rollbackSavePointID == transaction.savePointID {
			if err := transaction.RollbackToSavePoint(transaction.savePointID); err != nil {
				transaction.txSession.rollbackSavePointID = ""
				return err
			}
		}
		return nil
	case PROPAGATION_NOT_REQUIRED:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}
		if !transaction.isNested {
			err := transaction.txSession.Commit()
			if err != nil {
				return err
			}
		} else {
			return ErrNestedTransaction
		}
		return nil
	default:
		return ErrTransactionDefinition
	}
}

// Rollback When using transaction, you can rollback if any error
func (transaction *Transaction) RollbackTrans() error {
	switch transaction.transactionDefinition {
	case PROPAGATION_REQUIRED:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}
		if transaction.savePointID == "" {
			err := transaction.txSession.Rollback()
			if err != nil {
				return err
			}
		} else {
			transaction.txSession.rollbackSavePointID = transaction.savePointID
		}

		return nil
	case PROPAGATION_SUPPORTS:
		if transaction.IsExistingTransaction() {
			if transaction.savePointID == "" {
				err := transaction.txSession.Rollback()
				if err != nil {
					return err
				}
			} else {
				transaction.txSession.rollbackSavePointID = transaction.savePointID
			}
			return nil
		}
		return nil
	case PROPAGATION_MANDATORY:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}
		if transaction.savePointID == "" {
			err := transaction.txSession.Rollback()
			if err != nil {
				return err
			}
		} else {
			transaction.txSession.rollbackSavePointID = transaction.savePointID
		}
		return nil

	case PROPAGATION_REQUIRES_NEW:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}
		err := transaction.txSession.Rollback()
		if err != nil {
			return err
		}
		return nil
	case PROPAGATION_NOT_SUPPORTED:
		if transaction.IsExistingTransaction() {
			return ErrNestedTransaction
		}
		return nil
	case PROPAGATION_NEVER:
		if transaction.IsExistingTransaction() {
			return ErrNestedTransaction
		}
		return nil
	case PROPAGATION_NESTED:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}

		if transaction.txSession.rollbackSavePointID == transaction.savePointID {
			return nil
		}

		if transaction.isNested {
			if err := transaction.RollbackToSavePoint(transaction.savePointID); err != nil {
				return err
			}
			return nil
		} else {
			err := transaction.txSession.Rollback()
			if err != nil {
				return err
			}
			return nil
		}
	case PROPAGATION_NOT_REQUIRED:
		if !transaction.IsExistingTransaction() {
			return ErrNotInTransaction
		}

		err := transaction.txSession.Rollback()
		if err != nil {
			return err
		}
		return nil
	default:
		return ErrTransactionDefinition
	}
}

func (transaction *Transaction) SavePoint(savePointID string) error {
	if transaction.txSession.tx == nil {
		return ErrNotInTransaction
	}

	var lastSQL string
	dbtype := transaction.txSession.engine.Dialect().URI().DBType
	if dbtype == schemas.MSSQL {
		lastSQL = "save tran " + savePointID
	} else {
		lastSQL = "SAVEPOINT " + savePointID + ";"
	}

	transaction.txSession.saveLastSQL(lastSQL)
	if _, err := transaction.txSession.tx.Exec(lastSQL); err != nil {
		return err
	}

	return nil
}

func (transaction *Transaction) RollbackToSavePoint(savePointID string) error {
	if transaction.txSession.tx == nil {
		return ErrNotInTransaction
	}

	var lastSQL string
	dbtype := transaction.txSession.engine.Dialect().URI().DBType
	if dbtype == schemas.MSSQL {
		lastSQL = "rollback tran " + savePointID
	} else {
		lastSQL = "ROLLBACK TO SAVEPOINT " + transaction.savePointID + ";"
	}

	transaction.txSession.saveLastSQL(lastSQL)
	if _, err := transaction.txSession.tx.Exec(lastSQL); err != nil {
		return err
	}

	return nil
}
