/**
 * Mount helpers for Vue components
 *
 * @module mountUtils
 * @memberOf module:@unitybase/adminui-vue
 */
module.exports = {
  mountTab,
  mountModal,
  mountContainer,
  mountTableEntity
}

/* global $App, Ext */
const Vue = require('vue')
const UB = require('@unitybase/ub-pub')
const Dialog = require('element-ui').Dialog
const uDialogs = require('../uDialogs')

/**
 * Mount form in modal. Provide `isModal: true` to the child components, child components can inject it as `parentIsModal`
 *
 * @param {object} cfg
 * @param {Vue.Component} cfg.component Form component
 * @param {object} cfg.props Form component props
 * @param {object[]} [cfg.mixins] Form component mixins
 * @param {Vuex.Store} [cfg.store] Store
 * @param {string} cfg.title Title
 * @param {Validator} [cfg.validator] Validator
 * @param {string} [cfg.modalClass] Modal class
 * @param {string} [cfg.modalWidth] Modal width
 * @param {object} cfg.provide Regular object which provide all props what passed in it
 * @param {function} [cfg.onClose] Async callback, called (and awaited) before form is destroyed with 2 args:
 *    (ID: number|null, store: Vuex.Store); In case form is in isNew state, ID value is null, otherwise - an ID from store
 */
function mountModal ({
  component,
  mixins,
  props,
  store,
  title: titleText,
  validator,
  modalClass = 'ub-dialog__reset-padding',
  modalWidth,
  provide,
  onClose
}) {
  modalClass += ' ub-dialog__min-width'

  if (!modalWidth) {
    modalClass += ' ub-dialog__max-width'
  }
  const instance = new Vue({
    store,

    mixins,

    provide () {
      return {
        $v: validator ? validator.getValidationState() : undefined,
        validator,
        $formServices: this.$formServices,
        isModal: true,
        ...provide
      }
    },

    data () {
      return {
        dialogVisible: false,
        titleText
      }
    },

    computed: {
      isDirty () {
        return this.$store ? this.$store.getters.isDirty : false
      },

      isNew () {
        return this.$store ? this.$store.state.isNew : false
      },

      title () {
        const prefix = this.isDirty ? '* ' : ''
        const suffix = this.isNew ? ` (${UB.i18n('dobavlenie')})` : ''
        return prefix + this.$ut(this.titleText) + suffix
      },

      $formServices () {
        return {
          setTitle: this.setTitle,
          close: () => {
            beforeClose({
              close: () => {
                this.dialogVisible = false
              },
              store
            })
          },
          forceClose: () => {
            this.dialogVisible = false
          }
        }
      }
    },

    mounted () {
      if (this.$store && 'SET_FORM_SERVICES' in this.$store._mutations) {
        this.$store.commit('SET_FORM_SERVICES', this.$formServices)
      }
    },

    async beforeDestroy () {
      if (onClose && typeof onClose === 'function' && store) {
        await onClose(store.state.isNew ? null : store.state.data.ID, store)
      }
    },

    destroyed () {
      if (this.$el) {
        document.body.removeChild(this.$el)
      }
    },

    methods: {
      setTitle (value) {
        this.titleText = value
      }
    },

    render (h) {
      return h(Dialog, {
        ref: 'dialog',
        class: modalClass,
        props: {
          title: this.title,
          visible: this.dialogVisible,
          width: modalWidth,
          closeOnClickModal: false,
          beforeClose: (done) => {
            beforeClose({
              close: done,
              store
            })
          },
          ...props
        },
        on: {
          closed: () => { this.$destroy() },
          'update:visible': (val) => {
            this.dialogVisible = val
          }
        }
      }, [
        h(component, {
          props,
          on: {
            close: () => {
              this.dialogVisible = false
            }
          }
        })
      ])
    }
  })

  instance.$mount()
  document.body.appendChild(instance.$el)
  instance.dialogVisible = true
}

/**
 * Mount form in tab
 * @param {object} cfg
 * @param {Vue.Component} cfg.component Form component
 * @param {object} cfg.props Form component props
 * @param {object[]} [cfg.mixins] Form component mixins
 * @param {Vuex.Store} [cfg.store] Store
 * @param {string} cfg.title Title
 * @param {string} cfg.tabId navbar tab ID
 * @param {Validator} [cfg.validator] Validator
 * @param {string} [cfg.uiTag] Optional UI Tag for tracking subsystem
 * @param {object} cfg.provide Regular object which provide all props what passed in it
 * @param {boolean} [cfg.openInBackgroundTab=false] If `true` - the tab with a newly opened form does not become active
 * @param {function} [cfg.onClose] Async callback, called (and awaited) before form is destroyed with 2 args:
 *    (ID: number|null, store: Vuex.Store); In case form is in isNew state, ID value is null, otherwise - an ID from store
 */
function mountTab ({
  component,
  props,
  mixins,
  store,
  validator,
  title: titleText,
  titleTooltip: titleTooltipText,
  tabId,
  uiTag,
  provide,
  openInBackgroundTab,
  onClose
}) {
  const tabsCnt = $App.viewport.centralPanel.items.getCount()
  if (tabsCnt >= UB.connection.appConfig.uiSettings.adminUI.maxMainWindowTabOpened) {
    $App.dialogError('tabsCountLimitExceeded')
    return
  }

  const tab = $App.viewport.centralPanel.add({
    title: titleText,
    titleTooltip: titleTooltipText,
    id: tabId,
    closable: true,
    uiTag
  })

  const instance = new Vue({
    mixins,

    provide () {
      return {
        $v: validator ? validator.getValidationState() : undefined,
        validator,
        $formServices: this.$formServices,
        ...provide
      }
    },

    data () {
      return {
        titleText,
        titleTooltipText
      }
    },

    computed: {
      $formServices () {
        return {
          setTitle: this.setTitle,
          setTooltip: this.setTooltip,
          close: tab.close.bind(tab),
          forceClose () {
            tab.forceClose = true
            tab.close()
          }
        }
      },

      isDirty () {
        if (this.$store) {
          return this.$store.getters.isDirty
        } else {
          return false
        }
      },

      isNew () {
        if (this.$store) {
          return this.$store.state.isNew
        } else {
          return false
        }
      },

      title () {
        const prefix = this.isDirty ? '* ' : ''
        const suffix = this.isNew ? ` (${UB.i18n('dobavlenie')})` : ''
        return prefix + this.$ut(this.titleText) + suffix
      },

      titleTooltip () {
        return this.$ut(this.titleTooltipText) || this.title
      }
    },

    watch: {
      title: {
        immediate: true,
        handler (title) {
          tab.setTitle(title)
        }
      }
    },

    mounted () {
      if (this.$store && 'SET_FORM_SERVICES' in this.$store._mutations) {
        this.$store.commit('SET_FORM_SERVICES', this.$formServices)
      }
    },

    async beforeDestroy () {
      if (onClose && typeof onClose === 'function' && store) {
        await onClose(store.state.isNew ? null : store.state.data.ID, store)
      }
    },

    methods: {
      setTitle (title) {
        this.titleText = title
      },

      setTooltip (tooltip) {
        this.titleTooltipText = tooltip
        tab._formFullTitle = tooltip
      }
    },

    render: (h) => h(component, { props }),

    store
  })

  instance.$mount(`#${tab.getId()}-outerCt`) // simplify layouts by replacing Ext Panel inned content

  tab.on('close', () => instance.$destroy())

  tab.on('beforeClose', (currentTab) => {
    if (currentTab.forceClose) return true

    beforeClose({
      store,
      close: () => {
        tab.forceClose = true
        tab.close()
      }
    })

    return false
  })
  if (!openInBackgroundTab) {
    $App.viewport.centralPanel.setActiveTab(tab)
  }
}

/**
 * Check form isDirty, and is so - ask user to save od discard changes or continue to edit
 * @param {Store} store Store
 * @param {Function} close Callback for close
 */
function beforeClose ({ store, close }) {
  if (store) {
    if (store.getters.isDirty) {
      uDialogs.dialog({
        title: UB.i18n('unsavedData'),
        msg: UB.i18n('confirmSave'),
        type: 'warning',
        buttons: {
          yes: UB.i18n('save'),
          no: UB.i18n('doNotSave'),
          cancel: UB.i18n('cancel')
        }
      }).then(answer => {
        if (answer === 'yes') {
          if ('save' in store._actions) {
            store.dispatch('save').then(close)
          } else {
            close()
          }
        }
        if (answer === 'no') {
          close()
        }
      })
    } else {
      close()
    }
  } else {
    close()
  }
}

/**
 * Mount form directly into html container
 * @param {object} cfg
 * @param {Vue.Component} cfg.component Form component
 * @param {object} cfg.props Form component props
 * @param {object[]} [cfg.mixins] Form component mixins
 * @param {Vuex.Store} [cfg.store] Store
 * @param {object} cfg.provide Regular object which provide all props what passed in it
 * @param {Ext.component|String} cfg.target Either id of html element or Ext component
 * @param {Validator} [cfg.validator] validator
 * @param {function} [cfg.onClose] Async callback, called (and awaited) before form is destroyed with 2 args:
 *    (ID: number|null, store: Vuex.Store); In case form is in isNew state, ID value is null, otherwise - an ID from store
 */
function mountContainer ({
  component,
  props,
  mixins,
  store,
  provide,
  target,
  validator,
  onClose
}) {
  const $formServices = {
    setTitle () {},
    close () {},
    forceClose () { }
  }
  const instance = new Vue({
    store,
    mixins,
    provide () {
      return {
        $v: validator ? validator.getValidationState() : undefined,
        validator,
        $formServices,
        ...provide
      }
    },
    mounted () {
      if (this.$store && 'SET_FORM_SERVICES' in this.$store._mutations) {
        this.$store.commit('SET_FORM_SERVICES', $formServices)
      }
    },
    async beforeDestroy () {
      if (onClose && typeof onClose === 'function' && store) {
        await onClose(store.state.isNew ? null : store.state.data.ID, store)
      }
    },
    render: (h) => h(component, { props })
  })

  if (typeof target === 'string') {
    const el = document.querySelector(`#${target}`)
    if (!el) {
      instance.$notify({
        type: 'error',
        message: `Can't find html element with ${target} id`,
        duration: 3000
      })
      return
    }

    bindInstanceToParentDestroy(el, instance)
    instance.$mount(`#${target}`)
  } else if ('getId' in target) { // Ext component
    if (document.getElementById(`${target.getId()}-outerCt`)) {
      instance.$mount(`#${target.getId()}-outerCt`)
    } else if (document.getElementById(`${target.getId()}-innerCt`)) {
      instance.$mount(`#${target.getId()}-innerCt`)
    } else { // tab panel without fake element inside - use -body
      instance.$mount(`#${target.getId()}-body`)
    }

    // adding vue instance to basepanel
    const basePanel = target.up('basepanel')
    if (!basePanel.vueChilds) basePanel.vueChilds = []
    basePanel.vueChilds.push(instance)
    // this watcher helps parent ExtJS form to see vue form is dirty
    const unWatch = instance.$store
      ? instance.$store.watch(
        (state, getters) => getters.isDirty,
        () => basePanel.updateActions(),
        { immediate: true }
      )
      : null
    target.on('destroy', () => {
      if (unWatch) unWatch()
      instance.$destroy()
    })
  } else if (target instanceof HTMLElement) {
    bindInstanceToParentDestroy(target, instance)
    instance.$mount(target)
  }
}

const DESTROY_CHILD_SYMBOL = Symbol('destroy-child')

/**
 * @param {HTMLElement} element
 * @param {Vue} instance
 */
function bindInstanceToParentDestroy (element, instance) {
  const parentVueInstance = element.parentNode?.__vue__
  if (!parentVueInstance) {
    console.warn('Mounting target element does not contain parent with Vue instance. A potential memory leak exists because there is no hook to destroy the created instance')
    return
  }

  const prevInstance = element.__vue__
  if (prevInstance) {
    // prevent existing several instances for the same element
    prevInstance.$destroy()
    const prevDestroyCb = parentVueInstance[DESTROY_CHILD_SYMBOL]
    if (prevDestroyCb) {
      parentVueInstance.$off('hook:beforeDestroy', prevDestroyCb)
    }
  }
  parentVueInstance[DESTROY_CHILD_SYMBOL] = () => instance.$destroy()
  parentVueInstance.$once('hook:beforeDestroy', parentVueInstance[DESTROY_CHILD_SYMBOL])
}

const UMasterDetailView = require('../../components/UMasterDetailView/UMasterDetailView.vue').default

function getEntityName (cfg) {
  if (!cfg.props.entityName && !cfg.props.repository) {
    throw new Error('One of "props.entityName" or "props.repository" is required')
  }

  switch (typeof cfg.props.repository) {
    case 'function':
      return cfg.props.repository().entityName
    case 'object':
      return cfg.props.repository.entity
    default:
      return cfg.props.entityName
  }
}

/**
 * Mount UMasterDetailView
 *
 * @param {object} cfg Command config
 * @param {object} cfg.props Props data
 * @param {object} cfg.tabId Tab id
 * @param {string} [cfg.uiTag] Optional UI Tag for tracking subsystem
 * @param {string} [cfg.title] Tab title. Can contain macros `{attrName}`, such macros will be replaced by attributes values
 * @param {object} cfg.props UMasterDetailView props
 * @param {function} [cfg.props.repository] Function which returns ClientRepository.
 *   Can be empty in case `props.entityName` is defined - it this case repository constructed automatically
 *   based on attributes with `defaultView: true`
 * @param {string} [cfg.props.entityName] Name of entity. Ignored in case `props.repository` is defined
 * @param {array<string|UTableColumn>} [cfg.props.columns] Columns config.
 *   If empty will be constructed based on repository attributes.
 * @param {TableScopedSlotsBuilder} [cfg.scopedSlots] Scoped slots
 * @param {boolean} [cfg.isModal] Is modal
 * @param {string} [cfg.shortcutCode] Shortcut code
 */
function mountTableEntity (cfg) {
  const title = cfg.title || getEntityName(cfg)
  const tableRender = h => {
    const scopedSlots = cfg.scopedSlots && cfg.scopedSlots(h)
    return h(UMasterDetailView, {
      attrs: {
        ...cfg.props,
        shortcutCode: cfg.shortcutCode,
        isModal: cfg.isModal
      },
      style: { height: '100%' },
      scopedSlots
    })
  }

  if (cfg.isModal) {
    mountTableEntityAsModal({
      title: UB.i18n(title),
      tableRender
    })
  } else {
    mountTableEntityAsTab({
      title,
      tabId: cfg.tabId,
      uiTag: cfg.uiTag,
      tableRender
    })
  }
}

/**
 * Run UMasterDetailView as modal
 *
 * @param {object} cfg
 * @param {string} cfg.title Modal title. Can contain macros `{attrName}`, such macros will be replaced by attributes values
 * @param {function} cfg.tableRender UMasterDetailView render function
 * @param {string} [cfg.modalClass] Modal class
 * @param {string} [cfg.modalWidth] Modal width
 */
function mountTableEntityAsModal ({
  title,
  tableRender,
  modalClass = 'ub-dialog__reset-padding',
  modalWidth
}) {
  modalClass += ' ub-dialog__min-width ub-dialog__table-entity'

  if (!modalWidth) {
    modalClass += ' ub-dialog__max-width'
  }
  const instance = new Vue({
    data () {
      return {
        dialogVisible: false
      }
    },

    provide () {
      return {
        close: () => { this.dialogVisible = false }
      }
    },

    destroyed () {
      if (this.$el) {
        document.body.removeChild(this.$el)
      }
    },

    render (h) {
      return h(Dialog, {
        ref: 'dialog',
        class: modalClass,
        props: {
          title,
          visible: this.dialogVisible,
          width: modalWidth,
          closeOnClickModal: false
        },
        on: {
          open: () => {
            this.$nextTick(() => {
              const { dialog } = this.$refs
              const table = dialog.$el.querySelector('table')
              if (table) table.focus()
            })
          },
          closed: () => { this.$destroy() },
          'update:visible': (val) => {
            this.dialogVisible = val
          }
        }
      }, [tableRender(h)])
    }
  })
  instance.$mount()
  document.body.appendChild(instance.$el)
  instance.dialogVisible = true

  return instance
}

/**
 * Run UMasterDetailView as tab
 *
 * @param {object} cfg
 * @param {string} cfg.title Tab title. Can contain macros `{attrName}`, such macros will be replaced by attributes values
 * @param {string} cfg.tabId Navbar tab ID
 * @param {string} [cfg.uiTag] UI Tag for tracking subsystem
 * @param {function} cfg.tableRender UMasterDetailView render function
 */
function mountTableEntityAsTab ({
  title,
  tabId,
  uiTag,
  tableRender
}) {
  const existedTab = Ext.getCmp(tabId) || $App.viewport.centralPanel.down(`panel[tabID=${tabId}]`)
  if (existedTab) {
    $App.viewport.centralPanel.setActiveTab(existedTab)
  } else {
    const tabsCnt = $App.viewport.centralPanel.items.getCount()
    if (tabsCnt >= UB.connection.appConfig.uiSettings.adminUI.maxMainWindowTabOpened) {
      $App.dialogError('tabsCountLimitExceeded')
      return
    }
    const tab = $App.viewport.centralPanel.add({
      title: UB.i18n(title),
      id: tabId,
      closable: true,
      uiTag
    })

    const instance = new Vue({
      render: tableRender,
      provide: {
        close () {
          tab.forceClose = true
          tab.close()
        }
      }
    })

    tab.on('destroy', () => instance.$destroy())
    instance.$mount(`#${tab.getId()}-outerCt`)
    $App.viewport.centralPanel.setActiveTab(tab)
  }
}