{EventEmitter} = require 'events'

{TdsClient} = require './tds-client'
{TdsUtils} = require './tds-utils'

###*
Connection class for connecting to SQL Server
###
class exports.Connection extends EventEmitter

  constructor: (@_options) ->
    @_autoCommit = true
    @_client = new TdsClient
      error: (err) =>
        if @_pendingCallback?
          @_currentStatement = null
          cb = @_pendingCallback
          @_pendingCallback = null
          cb err
        else if @_pendingLoginCallback?
          cb = @_pendingLoginCallback
          @_pendingLoginCallback = null
          cb err
        else if @_currentStatement? then @_currentStatement._error err
        else if @handler? then @handler.error? err
        else @emit 'error', err
      message: (message) =>
        # turn errors into actual errors
        if message.error
          err = new TdsError message.text, message
          if @_pendingCallback?
            @_currentStatement = null
            cb = @_pendingCallback
            @_pendingCallback = null
            cb err
          else if @_pendingLoginCallback?
            cb = @_pendingLoginCallback
            @_pendingLoginCallback = null
            cb err
          else if @_currentStatement? then @_currentStatement._error err
          else if @handler? then @handler.error? err
          else @emit 'error', err
        else if @_currentStatement? then @_currentStatement._message message
        else if @handler? then @handler.message? message
        else @emit 'message', message
      connect: (connect) =>
        @_client.login @_options
      login: (login) =>
        cb = @_pendingLoginCallback
        @_pendingLoginCallback = null
        cb?()
      row: (row) =>
        @_currentStatement?._row row
      colmetadata: (colmetadata) =>
        @_currentStatement?._colmetadata colmetadata
      done: (done) =>
        if done.hasMore then return
        if @_pendingCallback?
          if @_currentStatement is '#setAutoCommit' then @_autoCommit = not @_autoCommit
          @_currentStatement = null
          cb = @_pendingCallback
          @_pendingCallback = null
          cb()
        else if @_currentStatement? then @_currentStatement._done done
      end: =>
        if @handler? then @handler.end?()
        else @emit 'end'
      close: (had_error) =>
        if @handler? then @handler.close? had_error
        else @emit 'close', had_error
    @_client.logError = @_options?.logError
    @_client.logDebug = @_options?.logDebug
  
  connect: (@_pendingLoginCallback) =>
    @_client.connect @_options

  createStatement: (sql, params, handler) =>
    if @_currentStatement then throw new Error 'Statement currently running'
    @_client.debug 'Creating statement: %s with params: ', sql, params
    new Statement @, sql, params, handler

  # TODO - build RPC call  
  createCall: (procName, params, handler) =>
    throw new Error 'Not yet implemented'

  # TODO - build bulk load (prebuilds colmetadata and done, does insert bulk call)
  # look at these two links for direction:
  #   http://sourceforge.net/projects/jbcp/
  #   https://github.com/mono/mono/blob/master/mcs/class/System.Data/System.Data.SqlClient/SqlBulkCopy.cs
  prepareBulkLoad: (tableName, batchSize, columns, cb) =>
    throw new Error 'Not yet implemented'

  setAutoCommit: (autoCommit, autoCommitCallback) =>
    # ignore if not changing anything
    if @_autoCommit is autoCommit
      cb()
    else
      if @_currentStatement?
        throw new Error 'Cannot change auto commit while statement is executing'
      @_pendingCallback = autoCommitCallback
      @_currentStatement = '#setAutoCommit'
      if autoCommit
        @_client.sqlBatch 'SET IMPLICIT_TRANSACTIONS OFF'
      else
        @_client.sqlBatch 'SET IMPLICIT_TRANSACTIONS ON'
  
  commit: (commitCallback) =>
    if @_autoCommit
      throw new Error 'Auto commit is on'
    if @_currentStatement?
      throw new Error 'Cannot commit while statement is executing'
    @_pendingCallback = commitCallback
    @_currentStatement = '#commit'
    @_client.sqlBatch 'IF @@TRANCOUNT > 0 COMMIT TRANSACTION'

  rollback: (rollbackCallback) =>
    if @_autoCommit
      throw new Error 'Auto commit is on'
    if @_currentStatement?
      throw new Error 'Cannot rollback while statement is executing'
    @_pendingCallback = rollbackCallback
    @_currentStatement = '#rollback'
    @_client.sqlBatch 'IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION'

  end: =>
    @_autoCommit = true
    @_pendingCallback = null
    @_currentStatement = null
    @_client.end()

###*
Statement class
###
Statement = class exports.Statement extends EventEmitter

  constructor: (@_connection, @_sql, @_params, @handler) ->
    if @_params?
      # build the parameter string
      @_parameterString = TdsUtils.buildParameterDefinition @_params

  prepare: (cb) ->
    if @_preparedHandle?
      throw new Error 'Statement already prepared'
    if not @_parameterString? or @_parameterString is ''
      # just tossing this here for best practices
      throw new Error 'Cannot prepare statement without parameters'
    if @_connection._currentStatement?
      throw new Error 'Another statement already executing'
    @_connection._currentStatement = @
    sql = "DECLARE @hndl Int;\nEXECUTE sp_prepare @hndl OUTPUT, N'" +
      @_parameterString + "',\nN'" + @_sql.replace(/'/g, "''") + 
      "\n', 1;\nSELECT @hndl;"
    @_pendingPrepareCallback = cb
    @_connection._client.sqlBatch sql

  unprepare: (cb) ->
    if not @_preparedHandle?
      throw new Error 'Statement not prepared'
    if @_connection._currentStatement?
      throw new Error 'Another statement already executing'
    @_connection._currentStatement = @
    @_pendingUnprepareCallback = cb
    @_connection._client.sqlBatch 'sp_unprepare ' + @_preparedHandle
  
  execute: (paramValues) =>
    if @_connection._currentStatement?
      throw new Error 'Another statement already executing'
    @_connection._currentStatement = @
    sql = null
    # regular?
    if not @_params? or @_parameterString is ''
      sql = @_sql
    else if @_preparedHandle?
      sql = 'EXECUTE sp_execute ' + @_preparedHandle + ', ' +
        TdsUtils.buildParameterizedSql @_params, paramValues
    else
      if not @_parameterizedSql?
        @_parameterizedSql = "EXECUTE sp_executesql \nN'" + 
          @_sql.replace(/'/g, "''") + "\n', N'" + @_parameterString + "'"
      sql = @_parameterizedSql + ', ' +
        TdsUtils.buildParameterizedSql @_params, paramValues
    @_connection._client.sqlBatch sql

  # TODO - send attention, ignore extra data until we receive notification of cancel
  cancel: =>
    if @_pendingPrepareCallback? or @_pendingUnprepareCallback?
      throw new Error 'Unable to cancel (un)prepare'
    @_cancelling = true
    @_connection._client.cancel()

  _error: (err) =>
    if @_pendingPrepareCallback?
      cb = @_pendingPrepareCallback
      @_pendingPrepareCallback = null
      @_connection.currentStatement = null
      cb err
      @_ignoreNextDone = true
    else if @_pendingUnprepareCallback?
      cb = @_pendingUnprepareCallback
      @_pendingUnprepareCallback = null
      @_connection.currentStatement = null
      cb err
      @_ignoreNextDone = true
    else if @handler? then @handler.error? err
    else @emit 'error', err

  _message: (message) =>
    if @handler? then @handler.message? message
    else @emit 'message', message

  _colmetadata: (colmetadata) =>
    if not @_cancelling and not @_pendingPrepareCallback? and not @_pendingUnprepareCallback?
      @metadata = colmetadata
      if @handler? then @handler.metadata? @metadata
      else @emit 'metadata', @metadata

  _row: (row) =>
    if @_pendingPrepareCallback?
      @_preparedHandle = row.getValue 0
    else if not @_cancelling
      if @handler? then @handler.row? row
      else @emit 'row', row

  _done: (done) =>
    if @_ignoreNextDone
      @_ignoreNextDone = undefined
    else if @_pendingPrepareCallback?
      cb = @_pendingPrepareCallback
      @_pendingPrepareCallback = null
      @_connection._currentStatement = null
      cb()
    else if @_pendingUnprepareCallback?
      cb = @_pendingUnprepareCallback
      @_pendingUnprepareCallback = null
      @_preparedHandle = undefined
      @_connection._currentStatement = null
      cb()
    else 
      if @_cancelling
        # TODO figure out why status doesn't show cancel
        @_cancelling = undefined
      @_connection._currentStatement = null
      if @handler? then @handler.done? done
      else @emit 'done', done

###*
TdsError class
###
TdsError = class exports.TdsError extends Error

  constructor: (@message, @info) ->
    @name = 'TdsError'
    @stack = (new Error).stack

