'use strict'

// TODO: only has to be a pointer packet if it's actually used.
const JsonPointer = require('json-ptr')
const debug = require('debug')('chix:packet')
const forOf = require('object-forof')

/**
 *
 * A Packet wraps the data.
 *
 * The packet is always owned by one owner at a time.
 *
 * In order to read or write a packet, the owner must identify
 * itself first, by sending itself als a reference as first argument
 * to any of the methods.
 *
 * const p = new Packet(data)
 *
 * Packet containing it's own map of source & target.
 * Could be possible, only what will happen on split.
 *
 */
let nr = 10000000000

// temp to debug copy problem
function Container (data) {
  this['.'] = data
}

class Packet {
  constructor (data, type, n, c, pp) {
    this.owner = undefined
    this.trail = []
    this.typeTrail = []
    this.pointerPath = pp || '/.'
    this._meta = {}

    this.type = type || typeof data

    // string must be assigned to? or {".": "string"}
    // which means base is /. instead of ''
    this.__data = data instanceof Container ? data : new Container(data)
    this.nr = n || nr++
    // clone version
    this.c = c || 0
    this.pointer = JsonPointer.create(this.pointerPath)

    Object.defineProperty(this, 'data', {
      get: function () {
        throw Error('data property should not be accessed')
      },
      set: function () {
        throw Error('data property should not be written to')
      }
    })

    this.created_at = new Date()
    this.updated_at = undefined
  }

  static create (data, type) {
    return new Packet(data, type)
  }

  /**
   *
   * Pointer is a JSON Pointer.
   *
   * If the pointer does not start with a slash
   * the pointer will be (forward) relative to the current position
   * If the pointer is empty the pointer will be to the root.
   *
   * @param {Object} owner
   * @param {String} pointer JSON Pointer
   */
  point (owner, pointer) {
    if (this.isOwner(owner)) {
      if (pointer === undefined || pointer === '') {
        this.pointerPath = '/.'
      } else if (pointer[0] === '/') {
        this.pointerPath = '/.' + pointer
      } else {
        this.pointerPath = this.pointer.pointer + '/' + pointer
      }
      this.pointer = JsonPointer.create(this.pointerPath)
      return this
    }
  }

  read (owner) {
    if (!this.hasOwner() || this.isOwner(owner)) {
      return this.pointer.get(this.__data)
    } else {
      throw Error('Not allowed to read packet')
    }
  }

  write (owner, data, type) {
    if (this.isOwner(owner)) {
      this.updated_at = new Date()
      this.pointer.set(this.__data, data)
      if (type) {
        this.type = type
      }
      return this
    } else {
      throw Error('Not allowed to write packet')
    }
  }

  /**
   * Clone the current packet
   *
   * In case of non plain objects it's mostly desired
   * not to clone the data itself, however do create a *new*
   * packet with the other cloned information.
   *
   * To enable this set cloneData to true.
   *
   * @param {Object} owner Owner of the packet
   * @param {Boolean} cloneData Whether or not to clone the data.
   */
  clone (owner) {
    if (this.isOwner(owner)) {
      const p = new Packet(
        null,
        null,
        this.nr,
        this.c + 1,
        this.pointerPath
      )
      p.setOwner(owner)
      if (!this.type) {
        throw Error('Refusing to clone substance of unknown type')
      }
      // TODO: make sure String Object are always lowercase.
      // I think they are..
      if (this.type === 'function' || /[A-Z]/.test(this.type)) {
        // do not clone, ownership will throw if things go wrong
        p.__data = this.__data
      } else {
        try {
          p.__data = JSON.parse(JSON.stringify(this.__data))
        } catch (e) {
          console.log('ERROR', this.__data, e)
          throw e
        }
      }
      p.type = this.type
      p._meta = JSON.parse(JSON.stringify(this._meta))
      return p
    }
  }

  static metaMerge (tgt, src) {
    forOf((ns, name, val) => {
      if (!tgt.hasOwnProperty(ns)) {
        tgt[ns] = {}
      }
      if (!tgt[ns].hasOwnProperty(name) || tgt[ns][name] === val) {
        tgt[ns][name] = val
      } else {
        console.log('tgt:', tgt, 'src:', src)
        console.log('oldValue: %s, newValue: %s', tgt[ns][name], val)
        throw Error(`Refusing to overwrite meta property ${ns}:${name}`)
      }
    }, src)
  }

  setType (owner, type) {
    if (this.isOwner(owner)) {
      this.typeTrail.push(this.type)
      this.type = type
      return this
    }
  }

  release (owner) {
    if (this.isOwner(owner)) {
      this.trail.push({
        id: owner.id,
        pid: owner.pid
      })
      this.owner = undefined
      return this
    }
  }

  setOwner (newOwner) {
    if (this.owner === undefined) {
      this.owner = newOwner
      return this
    } else {
      if (newOwner === this.owner) {
        debug('Warning: Packet already owned by this owner')
        return this
      } else {
        throw Error('Refusing to overwrite owner')
      }
    }
  }

  hasOwner () {
    return this.owner !== undefined
  }

  isOwner (owner) {
    if (owner === this.owner) {
      return true
    } else {
      if (!owner) {
        throw Error('Packet.isOwner expects an owner object as parameter')
      } else if (this.owner === undefined) {
        debug('Warning: Packet is unclaimed, claim it first')
        this.setOwner(owner)
        return true
      } else {
        // throw Error('Packet is not owned by this instance.')
        debug(
          'Warning: Packet is not owned by this instance. REQUESTOR: %s:%s OWNER: %s:%s',
          owner.constructor.name,
          owner.name || owner,
          this.owner.constructor.name,
          this.owner.name || this.owner
        )
        this.release(this.owner)
        this.setOwner(owner)
        return true
      }
    }
  }

  dump () {
    return JSON.stringify(this, null, 2)
  }

  // Are these used?
  set (prop, val) {
    this[prop] = val
  }

  get (prop) {
    return this[prop]
  }

  del (prop) {
    delete this[prop]
  }

  has (prop) {
    return this.hasOwnProperty(prop)
  }

  meta (ns, key, val) {
    if (val !== undefined) {
      if (!this._meta[ns]) {
        this._meta[ns] = {}
      }
      this._meta[ns][key] = val
      return this
    } else {
      if (!this._meta.hasOwnProperty(ns)) {
        throw Error('No such key ' + ns)
      }
      if (key) {
        if (!this._meta[ns].hasOwnProperty(key)) {
          throw Error('No such key [' + ns + '][' + key + ']')
        }
        return this._meta[ns][key]
      } else {
        return this._meta[ns]
      }
    }
  }

  hasMeta (ns, key) {
    if (!this._meta.hasOwnProperty(ns)) {
      return false
    }
    return this._meta[ns].hasOwnProperty(key)
  }

  removeMeta (ns, key) {
    if (this._meta.hasOwnProperty(ns)) {
      if (key) {
        if (this._meta[ns].hasOwnProperty(key)) {
          delete this._meta[ns][key]
        }
      } else {
        delete this._meta[ns]
      }
    }
  }

  // guard for two versions of Packet
  // instanceof would normally be sufficient
  static isPacket (p) {
    return p && typeof p === 'object' && (
        p instanceof Packet ||
        p.constructor.name === 'Packet'
      )
  }

  export () {
    return {
      type: this.type,
      owner: this.owner ? this.owner.id : null,
      trail: this.trail,
      typeTrail: this.typeTrail,
      data: this.pointer.get(this.__data),
      meta: this._meta,
      pointerPath: this.pointerPath,
      nr: this.nr,
      c: this.c
    }
  }
}

module.exports = Packet
