<script>
  export default {
    name: 'VabKeepAlive',
    abstract: true,
    props: {
      include: {
        type: [String, RegExp, Array],
        default: undefined,
      },
      max: {
        type: [String, Number],
        default: undefined,
      },
    },
    updated() {
      this.cacheVNode()
    },
    created() {
      this.keys = []
      this.cache = Object.create(null)
    },
    destroyed() {
      for (const key in this.cache) {
        this.pruneCacheEntry(this.cache, key, this.keys)
      }
    },
    mounted() {
      this.cacheVNode()
      this.$watch('include', (val) => {
        this.pruneCache(this, (name) => this.matches(val, name))
      })
    },
    methods: {
      cacheVNode() {
        const { cache, keys, vnodeToCache, keyToCache } = this
        if (vnodeToCache) {
          const { tag, componentInstance, componentOptions } = vnodeToCache
          cache[keyToCache] = {
            tag,
            componentInstance,
            name: this.getComponentName(componentOptions),
          }
          keys.push(keyToCache)
          if (this.max && keys.length > parseInt(this.max)) {
            this.pruneCacheEntry(cache, keys[0], keys, this._vnode)
          }
          this.vnodeToCache = null
        }
      },
      isDef(v) {
        return v !== undefined && v !== null
      },
      matches(pattern, name) {
        return pattern.indexOf(name) > -1
      },
      remove(arr, item) {
        if (arr.length) {
          const index = arr.indexOf(item)
          if (index > -1) {
            return arr.splice(index, 1)
          }
        }
      },
      isAsyncPlaceholder(node) {
        return node.isComment && node.asyncFactory
      },
      getFirstComponentChild(children) {
        if (Array.isArray(children)) {
          for (let i = 0; i < children.length; i++) {
            const c = children[i]
            if (
              this.isDef(c) &&
              (this.isDef(c.componentOptions) || this.isAsyncPlaceholder(c))
            ) {
              return c
            }
          }
        }
      },
      getComponentName(opts) {
        return opts && (opts.Ctor.options.name || opts.tag)
      },
      pruneCache(keepAliveInstance, filter) {
        const { cache, keys, vnode } = keepAliveInstance
        for (const key in cache) {
          const entry = cache[key]
          if (entry) {
            const name = entry.name
            if (name && !filter(name)) {
              this.pruneCacheEntry(cache, key, keys, vnode)
            }
          }
        }
      },
      pruneCacheEntry(cache, key, keys, current) {
        const entry = cache[key]
        if (entry && (!current || entry.tag !== current.tag)) {
          entry.componentInstance.$destroy()
        }
        cache[key] = null
        this.remove(keys, key)
      },
    },
    render() {
      const slot = this.$slots.default
      const vnode = this.getFirstComponentChild(slot)
      const componentOptions = vnode && vnode.componentOptions
      if (componentOptions) {
        const name = this.getComponentName(componentOptions)
        if (this.include && (!name || !this.matches(this.include, name))) {
          return vnode
        }
        const key =
          vnode.key == null
            ? componentOptions.Ctor.cid +
              (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key
        const cacheKey = `${componentOptions.Ctor.cid}::${key}`
        const { cache, keys } = this
        if (cache[cacheKey]) {
          vnode.componentInstance = cache[cacheKey].componentInstance
          this.remove(keys, cacheKey)
          keys.push(cacheKey)
        } else {
          this.keyToCache = cacheKey
          this.vnodeToCache = vnode
        }
        vnode.data.keepAlive = true
      }
      return vnode || (slot && slot[0])
    },
  }
</script>
