/* global document */

import { adapter as ProxyAdapterDom } from '../proxy-adapter-dom'

import { patchShowModal, getModalData } from './patch-page-show-modal'

patchShowModal()

// Svelte Native support
// =====================
//
// Rerendering Svelte Native page proves challenging...
//
// In NativeScript, pages are the top level component. They are normally
// introduced into NativeScript's runtime by its `navigate` function. This
// is how Svelte Natives handles it: it renders the Page component to a
// dummy fragment, and "navigate" to the page element thus created.
//
// As long as modifications only impact child components of the page, then
// we can keep the existing page and replace its content for HMR.
//
// However, if the page component itself is modified (including its system
// title bar), things get hairy...
//
// Apparently, the sole way of introducing a new page in a NS application is
// to navigate to it (no way to just replace it in its parent "element", for
// example). This is how it is done in NS's own "core" HMR.
//
// NOTE The last paragraph has not really been confirmed with NS6.
//
// Unfortunately the API they're using to do that is not public... Its various
// parts remain exposed though (but documented as private), so this exploratory
// work now relies on it. It might be fragile...
//
// The problem is that there is no public API that can navigate to a page and
// replace (like location.replace) the current history entry. Actually there
// is an active issue at NS asking for that. Incidentally, members of
// NativeScript-Vue have commented on the issue to weight in for it -- they
// probably face some similar challenge.
//
// https://github.com/NativeScript/NativeScript/issues/6283

const getNavTransition = ({ transition }) => {
  if (typeof transition === 'string') {
    transition = { name: transition }
  }
  return transition ? { animated: true, transition } : { animated: false }
}

// copied from TNS FrameBase.replacePage
//
// it is not public but there is a comment in there indicating it is for
// HMR (probably their own core HMR though)
//
// NOTE this "worked" in TNS 5, but not anymore in TNS 6: updated version bellow
//
// eslint-disable-next-line no-unused-vars
const replacePage_tns5 = (frame, newPageElement, hotOptions) => {
  const currentBackstackEntry = frame._currentEntry
  frame.navigationType = 2
  frame.performNavigation({
    isBackNavigation: false,
    entry: {
      resolvedPage: newPageElement.nativeView,
      //
      // entry: currentBackstackEntry.entry,
      entry: Object.assign(
        currentBackstackEntry.entry,
        getNavTransition(hotOptions)
      ),
      navDepth: currentBackstackEntry.navDepth,
      fragmentTag: currentBackstackEntry.fragmentTag,
      frameId: currentBackstackEntry.frameId,
    },
  })
}

// Updated for TNS v6
//
// https://github.com/NativeScript/NativeScript/blob/6.1.1/tns-core-modules/ui/frame/frame-common.ts#L656
const replacePage = (frame, newPageElement) => {
  const currentBackstackEntry = frame._currentEntry
  const newPage = newPageElement.nativeView
  const newBackstackEntry = {
    entry: currentBackstackEntry.entry,
    resolvedPage: newPage,
    navDepth: currentBackstackEntry.navDepth,
    fragmentTag: currentBackstackEntry.fragmentTag,
    frameId: currentBackstackEntry.frameId,
  }
  const navigationContext = {
    entry: newBackstackEntry,
    isBackNavigation: false,
    navigationType: 2 /* NavigationType replace */,
  }
  frame._navigationQueue.push(navigationContext)
  frame._processNextNavigationEntry()
}

export const adapter = class ProxyAdapterNative extends ProxyAdapterDom {
  constructor(instance) {
    super(instance)

    this.nativePageElement = null
    this.originalNativeView = null
    this.navigatedFromHandler = null

    this.relayNativeNavigatedFrom = this.relayNativeNavigatedFrom.bind(this)
  }

  dispose() {
    super.dispose()
    this.releaseNativePageElement()
  }

  releaseNativePageElement() {
    if (this.nativePageElement) {
      // native cleaning will happen when navigating back from the page
      this.nativePageElement = null
    }
  }

  // svelte-native uses navigateFrom event + e.isBackNavigation to know
  // when to $destroy the component -- but we don't want our proxy instance
  // destroyed when we renavigate to the same page for navigation purposes!
  interceptPageNavigation(pageElement) {
    const originalNativeView = pageElement.nativeView
    const { on } = originalNativeView
    const ownOn = originalNativeView.hasOwnProperty('on')
    // tricks svelte-native into giving us its handler
    originalNativeView.on = function(type, handler) {
      if (type === 'navigatedFrom') {
        this.navigatedFromHandler = handler
        if (ownOn) {
          originalNativeView.on = on
        } else {
          delete originalNativeView.on
        }
      } else {
        //some other handler wireup, we will just pass it on.
        if (on) {
          on(type, handler)
        }
      }
    }
  }

  afterMount(target, anchor) {
    // nativePageElement needs to be updated each time (only for page
    // components, native component that are not pages follow normal flow)
    //
    // TODO quid of components that are initially a page, but then have the
    // <page> tag removed while running? or the opposite?
    //
    // insertionPoint needs to be updated _only when the target changes_ --
    // i.e. when the component is mount, i.e. (in svelte3) when the component
    // is _created_, and svelte3 doesn't allow it to move afterward -- that
    // is, insertionPoint only needs to be created once when the component is
    // first mounted.
    //
    // TODO is it really true that components' elements cannot move in the
    // DOM? what about keyed list?
    //

    const isNativePage =
      (target.tagName === 'fragment' || target.tagName === 'frame') &&
      target.firstChild &&
      target.firstChild.tagName == 'page'
    if (isNativePage) {
      const nativePageElement = target.firstChild
      this.interceptPageNavigation(nativePageElement)
      this.nativePageElement = nativePageElement
    } else {
      // try to protect against components changing from page to no-page
      // or vice versa -- see DEBUG 1 above. NOT TESTED so prolly not working
      this.nativePageElement = null
      super.afterMount(target, anchor)
    }
  }

  rerender() {
    const { nativePageElement } = this
    if (nativePageElement) {
      this.rerenderNative()
    } else {
      super.rerender()
    }
  }

  rerenderNative() {
    const { nativePageElement: oldPageElement } = this
    const nativeView = oldPageElement.nativeView
    const frame = nativeView.frame
    if (frame) {
      return this.rerenderPage(frame, nativeView)
    }
    const modalParent = nativeView._modalParent // FIXME private API
    if (modalParent) {
      return this.rerenderModal(modalParent, nativeView)
    }
    // wtf? hopefully a race condition with a destroyed component, so
    // we have nothing more to do here
    //
    // for once, it happens when hot reloading dev deps, like this file
    //
  }

  rerenderPage(frame, previousPageView) {
    const isCurrentPage = frame.currentPage === previousPageView
    if (isCurrentPage) {
      const {
        instance: { hotOptions },
      } = this
      const newPageElement = this.createPage()
      if (!newPageElement) {
        throw new Error('Failed to create updated page')
      }
      const isFirstPage = !frame.canGoBack()

      if (isFirstPage) {
        // NOTE not so sure of bellow with the new NS6 method for replace
        //
        // The "replacePage" strategy does not work on the first page
        // of the stack.
        //
        // Resulting bug:
        // - launch
        // - change first page => HMR
        // - navigate to other page
        // - back
        //   => actual: back to OS
        //   => expected: back to page 1
        //
        // Fortunately, we can overwrite history in this case.
        //
        const nativeView = newPageElement.nativeView
        frame.navigate(
          Object.assign(
            {},
            {
              create: () => nativeView,
              clearHistory: true,
            },
            getNavTransition(hotOptions)
          )
        )
      } else {
        replacePage(frame, newPageElement, hotOptions)
      }
    } else {
      const backEntry = frame.backStack.find(
        ({ resolvedPage: page }) => page === previousPageView
      )
      if (!backEntry) {
        // well... looks like we didn't make it to history after all
        return
      }
      // replace existing nativeView
      const newPageElement = this.createPage()
      if (newPageElement) {
        backEntry.resolvedPage = newPageElement.nativeView
      } else {
        throw new Error('Failed to create updated page')
      }
    }
  }

  // modalParent is the page on which showModal(...) was called
  // oldPageElement is the modal content, that we're actually trying to reload
  rerenderModal(modalParent, modalView) {
    const modalData = getModalData(modalView)

    modalData.closeCallback = () => {
      const nativePageElement = this.createPage()
      if (!nativePageElement) {
        throw new Error('Failed to created updated modal page')
      }
      const { nativeView } = nativePageElement
      const { originalOptions } = modalData
      // Options will get monkey patched again, the only work left for us
      // is to try to reduce visual disturbances.
      //
      // FIXME Even that proves too much unfortunately... Apparently TNS
      // does not respect the `animated` option in this context:
      // https://docs.nativescript.org/api-reference/interfaces/_ui_core_view_base_.showmodaloptions#animated
      //
      const options = Object.assign({}, originalOptions, { animated: false })
      modalParent.showModal(nativeView, options)
    }

    modalView.closeModal()
  }

  createPage() {
    const {
      instance: { refreshComponent },
    } = this
    const { nativePageElement, relayNativeNavigatedFrom } = this
    const oldNativeView = nativePageElement.nativeView
    // rerender
    const target = document.createElement('fragment')
    // not using conservative for now, since there's nothing in place here to
    // leverage it (yet?) -- and it might be easier to miss breakages in native
    // only code paths
    refreshComponent(target, null)
    // this.nativePageElement is updated in afterMount, triggered by proxy / hooks
    const newPageElement = this.nativePageElement
    // update event proxy
    oldNativeView.off('navigatedFrom', relayNativeNavigatedFrom)
    nativePageElement.nativeView.on('navigatedFrom', relayNativeNavigatedFrom)
    return newPageElement
  }

  relayNativeNavigatedFrom({ isBackNavigation }) {
    const { originalNativeView, navigatedFromHandler } = this
    if (!isBackNavigation) {
      return
    }
    if (originalNativeView) {
      const { off } = originalNativeView
      const ownOff = originalNativeView.hasOwnProperty('off')
      originalNativeView.off = function() {
        this.navigatedFromHandler = null
        if (ownOff) {
          originalNativeView.off = off
        } else {
          delete originalNativeView.off
        }
      }
    }
    if (navigatedFromHandler) {
      return navigatedFromHandler.apply(this, arguments)
    }
  }

  renderError(err /* , target, anchor */) {
    // TODO fallback on TNS error handler for now... at least our error
    // is more informative
    throw err
  }
}
