'use strict'

const debug = require('debug')('mssql:base')
const { EventEmitter } = require('events')
const { IDS } = require('../utils')
const globalConnection = require('../global-connection')
const { TransactionError } = require('../error')
const shared = require('../shared')
const ISOLATION_LEVEL = require('../isolationlevel')

/**
 * Class Transaction.
 *
 * @property {Number} isolationLevel Controls the locking and row versioning behavior of TSQL statements issued by a connection. READ_COMMITTED by default.
 * @property {String} name Transaction name. Empty string by default.
 *
 * @fires Transaction#begin
 * @fires Transaction#commit
 * @fires Transaction#rollback
 */

class Transaction extends EventEmitter {
  /**
   * Create new Transaction.
   *
   * @param {Connection} [parent] If ommited, global connection is used instead.
   */

  constructor (parent) {
    super()

    IDS.add(this, 'Transaction')
    debug('transaction(%d): created', IDS.get(this))

    this.parent = parent || globalConnection.pool
    this.isolationLevel = Transaction.defaultIsolationLevel
    this.name = ''
  }

  get config () {
    return this.parent.config
  }

  get connected () {
    return this.parent.connected
  }

  /**
   * Acquire connection from connection pool.
   *
   * @param {Request} request Request.
   * @param {ConnectionPool~acquireCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise.
   * @return {Transaction|Promise}
   */

  acquire (request, callback) {
    if (!this._acquiredConnection) {
      setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN'))
      return this
    }

    if (this._activeRequest) {
      setImmediate(callback, new TransactionError("Can't acquire connection for the request. There is another request in progress.", 'EREQINPROG'))
      return this
    }

    this._activeRequest = request
    setImmediate(callback, null, this._acquiredConnection, this._acquiredConfig)
    return this
  }

  /**
   * Release connection back to the pool.
   *
   * @param {Connection} connection Previously acquired connection.
   * @return {Transaction}
   */

  release (connection) {
    if (connection === this._acquiredConnection) {
      this._activeRequest = null
    }

    return this
  }

  /**
   * Begin a transaction.
   *
   * @param {Number} [isolationLevel] Controls the locking and row versioning behavior of TSQL statements issued by a connection.
   * @param {basicCallback} [callback] A callback which is called after transaction has began, or an error has occurred. If omited, method returns Promise.
   * @return {Transaction|Promise}
   */

  begin (isolationLevel, callback) {
    if (isolationLevel instanceof Function) {
      callback = isolationLevel
      isolationLevel = undefined
    }

    if (typeof callback === 'function') {
      this._begin(isolationLevel, err => {
        if (!err) {
          this.emit('begin')
        }
        callback(err)
      })
      return this
    }

    return new shared.Promise((resolve, reject) => {
      this._begin(isolationLevel, err => {
        if (err) return reject(err)
        this.emit('begin')
        resolve(this)
      })
    })
  }

  /**
   * @private
   * @param {Number} [isolationLevel]
   * @param {basicCallback} [callback]
   * @return {Transaction}
   */

  _begin (isolationLevel, callback) {
    if (this._acquiredConnection) {
      return setImmediate(callback, new TransactionError('Transaction has already begun.', 'EALREADYBEGUN'))
    }

    this._aborted = false
    this._rollbackRequested = false
    if (isolationLevel) {
      if (Object.keys(ISOLATION_LEVEL).some(key => {
        return ISOLATION_LEVEL[key] === isolationLevel
      })) {
        this.isolationLevel = isolationLevel
      } else {
        throw new TransactionError('Invalid isolation level.')
      }
    }

    setImmediate(callback)
  }

  /**
   * Commit a transaction.
   *
   * @param {basicCallback} [callback] A callback which is called after transaction has commited, or an error has occurred. If omited, method returns Promise.
   * @return {Transaction|Promise}
   */

  commit (callback) {
    if (typeof callback === 'function') {
      this._commit(err => {
        if (!err) {
          this.emit('commit')
        }
        callback(err)
      })
      return this
    }

    return new shared.Promise((resolve, reject) => {
      this._commit(err => {
        if (err) return reject(err)
        this.emit('commit')
        resolve()
      })
    })
  }

  /**
   * @private
   * @param {basicCallback} [callback]
   * @return {Transaction}
   */

  _commit (callback) {
    if (this._aborted) {
      return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT'))
    }

    if (!this._acquiredConnection) {
      return setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN'))
    }

    if (this._activeRequest) {
      return setImmediate(callback, new TransactionError("Can't commit transaction. There is a request in progress.", 'EREQINPROG'))
    }

    setImmediate(callback)
  }

  /**
   * Returns new request using this transaction.
   *
   * @return {Request}
   */

  request () {
    return new shared.driver.Request(this)
  }

  /**
   * Rollback a transaction.
   *
   * @param {basicCallback} [callback] A callback which is called after transaction has rolled back, or an error has occurred. If omited, method returns Promise.
   * @return {Transaction|Promise}
   */

  rollback (callback) {
    if (typeof callback === 'function') {
      this._rollback(err => {
        if (!err) {
          this.emit('rollback', this._aborted)
        }
        callback(err)
      })
      return this
    }

    return new shared.Promise((resolve, reject) => {
      return this._rollback(err => {
        if (err) return reject(err)
        this.emit('rollback', this._aborted)
        resolve()
      })
    })
  }

  /**
   * @private
   * @param {basicCallback} [callback]
   * @return {Transaction}
   */

  _rollback (callback) {
    if (this._aborted) {
      return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT'))
    }

    if (!this._acquiredConnection) {
      return setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN'))
    }

    if (this._activeRequest) {
      return setImmediate(callback, new TransactionError("Can't rollback transaction. There is a request in progress.", 'EREQINPROG'))
    }

    this._rollbackRequested = true

    setImmediate(callback)
  }
}

/**
 * Default isolation level used for any transactions that don't explicitly specify an isolation level.
 *
 * @type {number}
 */
Transaction.defaultIsolationLevel = ISOLATION_LEVEL.READ_COMMITTED

module.exports = Transaction
