const { inherits } = require('util')

function Reporter (options) {
  this._reporterState = {
    obj: null,
    path: [],
    options: options || {},
    errors: []
  }
}

Reporter.prototype.isError = function isError (obj) {
  return obj instanceof ReporterError
}

Reporter.prototype.save = function save () {
  const state = this._reporterState

  return { obj: state.obj, pathLen: state.path.length }
}

Reporter.prototype.restore = function restore (data) {
  const state = this._reporterState

  state.obj = data.obj
  state.path = state.path.slice(0, data.pathLen)
}

Reporter.prototype.enterKey = function enterKey (key) {
  return this._reporterState.path.push(key)
}

Reporter.prototype.exitKey = function exitKey (index) {
  const state = this._reporterState

  state.path = state.path.slice(0, index - 1)
}

Reporter.prototype.leaveKey = function leaveKey (index, key, value) {
  const state = this._reporterState

  this.exitKey(index)
  if (state.obj !== null) { state.obj[key] = value }
}

Reporter.prototype.path = function path () {
  return this._reporterState.path.join('/')
}

Reporter.prototype.enterObject = function enterObject () {
  const state = this._reporterState

  const prev = state.obj
  state.obj = {}
  return prev
}

Reporter.prototype.leaveObject = function leaveObject (prev) {
  const state = this._reporterState

  const now = state.obj
  state.obj = prev
  return now
}

Reporter.prototype.error = function error (msg) {
  let err
  const state = this._reporterState

  const inherited = msg instanceof ReporterError
  if (inherited) {
    err = msg
  } else {
    err = new ReporterError(state.path.map(function (elem) {
      return `[${JSON.stringify(elem)}]`
    }).join(''), msg.message || msg, msg.stack)
  }

  if (!state.options.partial) { throw err }

  if (!inherited) { state.errors.push(err) }

  return err
}

Reporter.prototype.wrapResult = function wrapResult (result) {
  const state = this._reporterState
  if (!state.options.partial) { return result }

  return {
    result: this.isError(result) ? null : result,
    errors: state.errors
  }
}

function ReporterError (path, msg) {
  this.path = path
  this.rethrow(msg)
}
inherits(ReporterError, Error)

ReporterError.prototype.rethrow = function rethrow (msg) {
  this.message = `${msg} at: ${this.path || '(shallow)'}`
  if (Error.captureStackTrace) { Error.captureStackTrace(this, ReporterError) }

  if (!this.stack) {
    try {
      // IE only adds stack when thrown
      throw new Error(this.message)
    } catch (e) {
      this.stack = e.stack
    }
  }
  return this
}

exports.Reporter = Reporter
