# src/Types/YMap/YMap.js

    import Type from '../../Struct/Type.js'
    import Item from '../../Struct/Item.js'
    import ItemJSON from '../../Struct/ItemJSON.js'
    import { logItemHelper } from '../../MessageHandler/messageToString.js'
    import YEvent from '../../Util/YEvent.js'

    /**
     * Event that describes the changes on a YMap.
     *
     * @param {YMap} ymap The YArray that changed.
     * @param {Set<any>} subs The keys that changed.
     * @param {boolean} remote Whether the change was created by a remote peer.
     */
    export class YMapEvent extends YEvent {
      constructor (ymap, subs, remote) {
        super(ymap)
        this.keysChanged = subs
        this.remote = remote
      }
    }

    /**
     * A shared Map implementation.
     */
    export default class YMap extends Type {
      /**
       * @private
       * Creates YMap Event and calls observers.
       */
      _callObserver (transaction, parentSubs, remote) {
        this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
      }

      /**
       * Transforms this Shared Type to a JSON object.
       *
       * @return {Object}
       */
      toJSON () {
        const map = {}
        for (let [key, item] of this._map) {
          if (!item._deleted) {
            let res
            if (item instanceof Type) {
              if (item.toJSON !== undefined) {
                res = item.toJSON()
              } else {
                res = item.toString()
              }
            } else {
              res = item._content[0]
            }
            map[key] = res
          }
        }
        return map
      }

      /**
       * Returns the keys for each element in the YMap Type.
       *
       * @return {Array}
       */
      keys () {
        // TODO: Should return either Iterator or Set!
        let keys = []
        for (let [key, value] of this._map) {
          if (!value._deleted) {
            keys.push(key)
          }
        }
        return keys
      }

      /**
       * Remove a specified element from this YMap.
       *
       * @param {encodable} key The key of the element to remove.
       */
      delete (key) {
        this._transact((y) => {
          let c = this._map.get(key)
          if (y !== null && c !== undefined) {
            c._delete(y)
          }
        })
      }

      /**
       * Adds or updates an element with a specified key and value.
       *
       * @param {encodable} key The key of the element to add to this YMap.
       * @param {encodable | YType} value The value of the element to add to this
       *                                  YMap.
       */
      set (key, value) {
        this._transact(y => {
          const old = this._map.get(key) || null
          if (old !== null) {
            if (
              old.constructor === ItemJSON &&
              !old._deleted && old._content[0] === value
            ) {
              // Trying to overwrite with same value
              // break here
              return value
            }
            if (y !== null) {
              old._delete(y)
            }
          }
          let v
          if (typeof value === 'function') {
            v = new value() // eslint-disable-line new-cap
            value = v
          } else if (value instanceof Item) {
            v = value
          } else {
            v = new ItemJSON()
            v._content = [value]
          }
          v._right = old
          v._right_origin = old
          v._parent = this
          v._parentSub = key
          if (y !== null) {
            v._integrate(y)
          } else {
            this._map.set(key, v)
          }
        })
        return value
      }

      /**
       * Returns a specified element from this YMap.
       *
       * @param {encodable} key The key of the element to return.
       */
      get (key) {
        let v = this._map.get(key)
        if (v === undefined || v._deleted) {
          return undefined
        }
        if (v instanceof Type) {
          return v
        } else {
          return v._content[v._content.length - 1]
        }
      }

      /**
       * Returns a boolean indicating whether the specified key exists or not.
       *
       * @param {encodable} key The key to test.
       */
      has (key) {
        let v = this._map.get(key)
        if (v === undefined || v._deleted) {
          return false
        } else {
          return true
        }
      }

      /**
       * Transform this YXml Type to a readable format.
       * Useful for logging as all Items and Delete implement this method.
       *
       * @private
       */
      _logString () {
        return logItemHelper('YMap', this, `mapSize:${this._map.size}`)
      }
    }