/**
 * 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 UDrawer =
  require('../../components/controls/UDrawer/UDrawer.vue').default
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.modalId] Modal id, needed for detect same dialog and show/maximize if it was hidden/minimized
 * @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.beforeClose] Callback, called before form is closed (not forcefully).
 *   This implementation will replace the standard one, which asks the user to save or discard changes or continue to edit
 * @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
 * @param cfg.modalType
 * @param cfg.target
 */
function mountModal (cfg) {
  let {
    component,
    mixins,
    props,
    store,
    title: titleText,
    validator,
    modalId,
    modalClass = 'ub-dialog__reset-padding',
    modalWidth,
    modalType = 'dialog', // 'dialog' or 'drawer'
    provide,
    beforeClose = baseBeforeClose,
    onClose
  } = cfg

  if (modalId) {
    const modal = $App.viewport.centralPanel.ModalManager.getModalByModalId(modalId)

    if (modal) {
      modal.componentInstance.dialogVisible = true

      if (modalType === 'drawer') {
        modal.componentInstance.$refs.dialog.toggleMinimized()
      }

      return
    }
  }

  modalClass += ' ub-dialog__min-width'

  if (!modalWidth) {
    modalClass += ' ub-dialog__max-width'
  }
  const instance = Vue.component('ModalComponent', {
    devtoolsTags: ['form', 'modal', ...(store ? ['customStore'] : [])],
    cfg,

    store,

    mixins,

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

    data () {
      return {
        modalId,
        dialogVisible: false,
        titleText,
        localIsDirty: undefined,
        localSaveAction: undefined,
        localCloseAction: () => {},
        closing: false
      }
    },

    computed: {
      isDirty () {
        if (this.localIsDirty !== undefined) {
          return this.localIsDirty
        } else if (this.$store) {
          return this.$store.getters.isDirty
        }

        return 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,
          setLocalSaveAction: (saveAction) => {
            this.localSaveAction = saveAction
          },
          setLocalCloseAction: (closeAction) => {
            this.localCloseAction = closeAction
          },
          setLocalIsDirty: (isDirty) => {
            this.localIsDirty = isDirty
          },
          close: (argBeforeClose = beforeClose) => {
            this.closing = true
            argBeforeClose({
              close: () => {
                this.closing = false
                this.dialogVisible = false
                this.localCloseAction()
              },
              save: () => {
                this.closing = false
                this.localSaveAction()
              },
              cancel: () => {
                this.closing = false
              },
              store,
              isDirty: this.isDirty
            })
          },
          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)
      }
    },

    methods: {
      setVisible (val) {
        this.dialogVisible = val
      },

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

    render (h) {
      let modalComponent = Dialog

      let componentProps = {
        title: this.title,
        visible: this.dialogVisible,
        width: modalWidth,
        closeOnClickModal: false,
        beforeClose: (done) => {
          this.closing = true
          beforeClose({
            close: () => {
              this.closing = false
              done()
              this.localCloseAction()
            },
            save: () => {
              this.closing = false
              this.localSaveAction()
            },
            cancel: () => {
              this.closing = false
            },
            store,
            isDirty: this.isDirty
          })
        },
        closing: this.closing,
        ...props
      }
      let modalListeners = {
        closed: () => {
          $App.viewport.centralPanel.ModalManager.unregisterModal(this)
        },
        'update:visible': this.setVisible
      }

      let scopedSlots = {}
      const children = []
      const componentListeners = {
        close: () => {
          this.dialogVisible = false
        }
      }

      switch (modalType) {
        case 'drawer':
          modalComponent = UDrawer
          componentProps = { ...componentProps, size: modalWidth }
          modalListeners = {
            ...modalListeners,
            open: () => this.setVisible(true),
            close: () => this.setVisible(false)
          }
          scopedSlots = {
            ...scopedSlots,
            default: (scopedProps) =>
              h(component, {
                props: { ...scopedProps, ...props, title: this.title },
                on: componentListeners
              })
          }
          break

        default:
          children.push(
            h(component, {
              props,
              on: componentListeners
            })
          )
          break
      }

      return h(
        modalComponent,
        {
          ref: 'dialog',
          class: modalClass,
          props: componentProps,
          on: modalListeners,
          scopedSlots
        },
        children
      )
    }
  })

  $App.viewport.centralPanel.ModalManager.registerModal(instance).then(
    (modal) => {
      modal.componentInstance.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.beforeClose] Callback, called before form is closed (not forcefully).
 *   This implementation will replace the standard one, which asks the user to save or discard changes or continue to edit
 * @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
 * @param cfg.iconCls
 * @param cfg.commandConfig
 */
async function mountTab (cfg) {
  const {
    component,
    props,
    mixins,
    store,
    validator,
    title: titleText,
    titleTooltip: titleTooltipText,
    tabId,
    uiTag,
    provide,
    openInBackgroundTab,
    iconCls,
    commandConfig,
    beforeClose = baseBeforeClose,
    onClose
  } = cfg

  const items = $App.viewport.centralPanel.items
  const tabsCnt = items.length

  if (
    tabsCnt >= UB.connection.appConfig.uiSettings.adminUI.maxMainWindowTabOpened
  ) {
    $App.dialogError('tabsCountLimitExceeded')
    return
  }

  const newTabId = `${tabId}`

  const existedTab = $App.viewport.centralPanel.getTabById(newTabId)

  if (existedTab) {
    $App.viewport.centralPanel.setActiveTab(existedTab)

    return
  }

  const tab = $App.viewport.centralPanel.add({
    commandConfig,
    iconCls,
    title: titleText,
    titleTooltip: titleTooltipText,
    id: newTabId,
    closable: true,
    uiTag,
    renderFn: (h) => {
      return h('div')
    }
  })

  const instanceComponent = {
    devtoolsTags: [
      'form',
      ...(store ? ['customStore'] : []),
      `entity:${commandConfig?.entity}`,
      `formCode:${commandConfig?.formCode}`
    ],
    cfg,

    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.setTooltip(tooltip)
      }
    },

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

    store
  }

  const instance = Vue.component('MountTabWrapper', instanceComponent)

  $App.viewport.centralPanel.changeTab({
    tabData: {
      id: tabId,
      renderFn: (h) => h(instance)
    }
  })

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

    this.closing = true
    beforeClose({
      store,
      close: () => {
        this.closing = false
        tab.forceClose = true
        tab.close()
      },
      cancel: () => {
        this.closing = false
      },
      isDirty: store?.getters?.isDirty || false
    })

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

/**
 * Show confirmation dialog
 * @param {object} args
 * @param {Vuex.Store} args.store
 * @param {Function} args.close
 * @param args.saveAction
 * @param args.cancel
 */
function showConfirmationDialog ({ saveAction, close, cancel = () => {} }) {
  const buttons = {
    yes: saveAction ? UB.i18n('save') : UB.i18n('return'),
    no: saveAction ? UB.i18n('doNotSave') : UB.i18n('close')
  }

  if (saveAction) {
    buttons.cancel = UB.i18n('cancel')
  }

  uDialogs.dialog({
    title: saveAction ? UB.i18n('unsavedData') : UB.i18n('warning!'),
    msg: saveAction ? UB.i18n('confirmSave') : `${UB.i18n('lostData')} ${UB.i18n('doYouWantToContinue')}`,
    type: 'warning',
    buttons,
    customClass: 'ub-dialog__confirmation'
  }).then(answer => {
    if (answer === 'yes' && saveAction) {
      saveAction()?.then(close)
    }

    if (answer === 'yes' && !saveAction) {
      close()
    }

    if (answer === 'no') {
      close()
    }

    if (answer === 'cancel') {
      cancel()
    }
  })
}

/**
 * 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 baseBeforeClose ({ store, close, save, cancel = () => {}, isDirty }) {
  const saveAction = save || (store && 'save' in store._actions ? store.dispatch.bind(store, 'save') : null)

  if (isDirty) {
    showConfirmationDialog({ saveAction, close, cancel })
  } 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 (cfg) {
  const {
    component,
    props,
    mixins,
    store,
    provide,
    target,
    validator,
    onClose
  } = cfg
  const $formServices = {
    setTitle () {},
    close () {},
    forceClose () { }
  }
  const componentConfig = {
    devtoolsTags: ['form', 'container', ...(store ? ['customStore'] : [])],
    cfg,
    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 })
  }

  const instance = target._isVue
    ? Vue.component('ContainerWrapper', componentConfig)
    : new Vue(componentConfig)

  if (target._isVue) {
    target.previewComponent = instance
  } else 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

/**
 *
 * @param cfg
 */
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
 * @param {string} [cfg.uiSettingKey] Used as a key part for storing table UI settings (ordering, column widths and visibility) into local storage.
 *   If empty (default) columns is not configurable
 */
function mountTableEntity (cfg) {
  const mountTableEventParams = { cfg, entityName: getEntityName(cfg), continue: true }
  /**
   * Fired before the `showList` (dictionary shown) command. Handler can cancel command by set `params.continue = false`
   *
   * @example
   *   window.$App.on('portal:mountTableEntity:before', (params) => {
   *     if (params.entityName.startsWith('org_') && !$App.connection.userData.hasRole('LocalOrgManager')) {
   *       params.continue = false // prevent show dictionary
   *       $App.dialogError('recordNotExistsOrDontHaveRights')
   *     }
   *   })
   * @event 'portal:mountTableEntity:before'
   * @memberOf module:@unitybase/adminui-vue
   * @param {object} cfg Config, passed to showList
   * @param {string} entityName Name of entity command runs for
   * @param {boolean} continue Initial value is `true`. Event handler can set is ot `false` to prevent command execution
   */
  $App.fireEvent('portal:mountTableEntity:before', mountTableEventParams)
  if (!mountTableEventParams.continue) return // some of the event handlers may stop the process
  const title = cfg.title || getEntityName(cfg)
  const tableRender = h => {
    const scopedSlots = cfg.scopedSlots && cfg.scopedSlots(h)
    return h(UMasterDetailView, {
      devtoolsTags: ['tableEntity', getEntityName(cfg)],
      cfg,
      attrs: {
        ...cfg.props,
        shortcutCode: cfg.shortcutCode,
        uiSettingKey: cfg.uiSettingKey || cfg.shortcutCode,
        isModal: cfg.isModal
      },
      style: { height: '100%' },
      scopedSlots
    })
  }

  if (cfg.isModal) {
    mountTableEntityAsModal({
      title: UB.i18n(title),
      tableRender
    })
  } else {
    mountTableEntityAsTab({
      title,
      iconCls: cfg.iconCls,
      commandConfig: cfg.commandConfig,
      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({
    devtoolsTags: ['modal', 'tableEntity'],

    cfg: {
      title,
      tableRender,
      modalClass,
      modalWidth
    },

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

    data () {
      return {
        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
 * @param cfg.iconCls
 * @param cfg.commandConfig
 */
function mountTableEntityAsTab ({
  title,
  tabId,
  uiTag,
  iconCls,
  commandConfig,
  tableRender
}) {
  const existedTab = $App.viewport.centralPanel.getTabById(tabId)

  if (existedTab) {
    $App.viewport.centralPanel.setActiveTab(existedTab)
  } else {
    const items = $App.viewport.centralPanel.items
    const tabsCnt = items.length
    if (tabsCnt >= UB.connection.appConfig.uiSettings.adminUI.maxMainWindowTabOpened) {
      $App.dialogError('tabsCountLimitExceeded')
      return
    }
    const tab = $App.viewport.centralPanel.add({
      commandConfig,
      iconCls,
      title: UB.i18n(title),
      id: tabId,
      closable: true,
      uiTag,
      renderFn: tableRender
    })

    $App.viewport.centralPanel.setActiveTab(tab)
  }
}