'use strict'

const assert = require('assert')
const http = require('http')
const Handlers = buildHandlers()

const types = {
  STATIC: 0,
  PARAM: 1,
  MATCH_ALL: 2,
  REGEX: 3,
  // It's used for a parameter, that is followed by another parameter in the same part
  MULTI_PARAM: 4
}

function Node (options) {
  // former arguments order: prefix, children, kind, handlers, regex, versions
  options = options || {}
  this.prefix = options.prefix || '/'
  this.label = this.prefix[0]
  this.children = options.children || {}
  this.numberOfChildren = Object.keys(this.children).length
  this.kind = options.kind || this.types.STATIC
  this.handlers = new Handlers(options.handlers)
  this.regex = options.regex || null
  this.wildcardChild = null
  this.parametricBrother = null
  this.versions = options.versions
}

Object.defineProperty(Node.prototype, 'types', {
  value: types
})

Node.prototype.getLabel = function () {
  return this.prefix[0]
}

Node.prototype.addChild = function (node) {
  var label = ''
  switch (node.kind) {
    case this.types.STATIC:
      label = node.getLabel()
      break
    case this.types.PARAM:
    case this.types.REGEX:
    case this.types.MULTI_PARAM:
      label = ':'
      break
    case this.types.MATCH_ALL:
      this.wildcardChild = node
      label = '*'
      break
    default:
      throw new Error(`Unknown node kind: ${node.kind}`)
  }

  assert(
    this.children[label] === undefined,
    `There is already a child with label '${label}'`
  )

  this.children[label] = node
  this.numberOfChildren = Object.keys(this.children).length

  const labels = Object.keys(this.children)
  var parametricBrother = this.parametricBrother
  for (var i = 0; i < labels.length; i++) {
    const child = this.children[labels[i]]
    if (child.label === ':') {
      parametricBrother = child
      break
    }
  }

  // Save the parametric brother inside static children
  const iterate = (node) => {
    if (!node) {
      return
    }

    if (node.kind !== this.types.STATIC) {
      return
    }

    if (node !== this) {
      node.parametricBrother = parametricBrother || node.parametricBrother
    }

    const labels = Object.keys(node.children)
    for (var i = 0; i < labels.length; i++) {
      iterate(node.children[labels[i]])
    }
  }

  iterate(this)

  return this
}

Node.prototype.reset = function (prefix, versions) {
  this.prefix = prefix
  this.children = {}
  this.kind = this.types.STATIC
  this.handlers = new Handlers()
  this.numberOfChildren = 0
  this.regex = null
  this.wildcardChild = null
  this.versions = versions
  return this
}

Node.prototype.findByLabel = function (path) {
  return this.children[path[0]]
}

Node.prototype.findChild = function (path, method) {
  var child = this.children[path[0]]
  if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) {
    if (path.slice(0, child.prefix.length) === child.prefix) {
      return child
    }
  }

  child = this.children[':']
  if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) {
    return child
  }

  child = this.children['*']
  if (child !== undefined && (child.numberOfChildren > 0 || child.handlers[method] !== null)) {
    return child
  }

  return null
}

Node.prototype.findVersionChild = function (version, path, method) {
  var child = this.children[path[0]]
  if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) {
    if (path.slice(0, child.prefix.length) === child.prefix) {
      return child
    }
  }

  child = this.children[':']
  if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) {
    return child
  }

  child = this.children['*']
  if (child !== undefined && (child.numberOfChildren > 0 || child.getVersionHandler(version, method) !== null)) {
    return child
  }

  return null
}

Node.prototype.setHandler = function (method, handler, params, store) {
  if (!handler) return

  assert(
    this.handlers[method] !== undefined,
    `There is already an handler with method '${method}'`
  )

  this.handlers[method] = {
    handler: handler,
    params: params,
    store: store || null,
    paramsLength: params.length
  }
}

Node.prototype.setVersionHandler = function (version, method, handler, params, store) {
  if (!handler) return

  const handlers = this.versions.get(version) || new Handlers()
  assert(
    handlers[method] === null,
    `There is already an handler with version '${version}' and method '${method}'`
  )

  handlers[method] = {
    handler: handler,
    params: params,
    store: store || null,
    paramsLength: params.length
  }
  this.versions.set(version, handlers)
}

Node.prototype.getHandler = function (method) {
  return this.handlers[method]
}

Node.prototype.getVersionHandler = function (version, method) {
  var handlers = this.versions.get(version)
  return handlers === null ? handlers : handlers[method]
}

Node.prototype.prettyPrint = function (prefix, tail) {
  var paramName = ''
  var handlers = this.handlers || {}
  var methods = Object.keys(handlers).filter(method => handlers[method] && handlers[method].handler)

  if (this.prefix === ':') {
    methods.forEach((method, index) => {
      var params = this.handlers[method].params
      var param = params[params.length - 1]
      if (methods.length > 1) {
        if (index === 0) {
          paramName += param + ` (${method})\n`
          return
        }
        paramName += prefix + '    :' + param + ` (${method})`
        paramName += (index === methods.length - 1 ? '' : '\n')
      } else {
        paramName = params[params.length - 1] + ` (${method})`
      }
    })
  } else if (methods.length) {
    paramName = ` (${methods.join('|')})`
  }

  var tree = `${prefix}${tail ? '└── ' : '├── '}${this.prefix}${paramName}\n`

  prefix = `${prefix}${tail ? '    ' : '│   '}`
  const labels = Object.keys(this.children)
  for (var i = 0; i < labels.length - 1; i++) {
    tree += this.children[labels[i]].prettyPrint(prefix, false)
  }
  if (labels.length > 0) {
    tree += this.children[labels[labels.length - 1]].prettyPrint(prefix, true)
  }
  return tree
}

function buildHandlers (handlers) {
  var code = `handlers = handlers || {}
  `
  for (var i = 0; i < http.METHODS.length; i++) {
    var m = http.METHODS[i]
    code += `this['${m}'] = handlers['${m}'] || null
    `
  }
  return new Function('handlers', code) // eslint-disable-line
}

module.exports = Node
module.exports.Handlers = Handlers
