import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, Inject, Optional, ChangeDetectionStrategy } from "@angular/core";
import { fromEvent, Subscription, ReplaySubject, BehaviorSubject, Observable, race, timer, Subject } from 'rxjs'
import { debounceTime, filter, map, scan, startWith, mapTo, switchMap, take, skip, tap, distinctUntilChanged } from "rxjs/operators";
import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
import { StateInterface as ViewerConfiguration } from "src/services/state/viewerConfig.store";
import { LoggingService } from "src/logging";
import { bufferUntil, getExportNehuba, getViewer, setNehubaViewer, switchMapWaitFor } from "src/util/fn";
import { deserializeSegment, NEHUBA_INSTANCE_INJTKN, scanSliceViewRenderFn } from "../util";
import { arrayOrderedEql } from 'common/util'
import { IMeshesToLoad, SET_MESHES_TO_LOAD } from "../constants";
import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";

/**
 * import of nehuba js files moved to angular.json
 */
import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl } from "../layerCtrl.service/layerCtrl.util";

const NG_LANDMARK_LAYER_NAME = 'spatial landmark layer'
const NG_USER_LANDMARK_LAYER_NAME = 'user landmark layer'

/**
 * optimized for nehubaConfig.layout.useNehubaPerspective.fixedZoomPerspectiveSlices
 *  sliceZoom
 *  sliceViewportWidth
 *  sliceViewportHeight
 */
const NG_LANDMARK_CONSTANT = 1e-8

export const IMPORT_NEHUBA_INJECT_TOKEN = `IMPORT_NEHUBA_INJECT_TOKEN`

interface LayerLabelIndex {
  layer: {
    name: string
  }

  labelIndicies: number[]
}

export interface INehubaLifecycleHook{
  onInit?: () => void
}

export const scanFn = (acc: LayerLabelIndex[], curr: LayerLabelIndex) => {
  const found = acc.find(layerLabelIndex => {
    return layerLabelIndex.layer.name === curr.layer.name
  })
  if (!found) {
    return [ ...acc, curr ]
  }
  return acc.map(layerLabelIndex => {
    return layerLabelIndex.layer.name === curr.layer.name
      ? curr
      : layerLabelIndex
  })
}

/**
 * no selector is needed, as currently, nehubaviewer is created dynamically
 */
@Component({
  templateUrl : './nehubaViewer.template.html',
  styleUrls : [
    './nehubaViewer.style.css',
  ],
  // OnPush seems to improve performance significantly
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class NehubaViewerUnit implements OnInit, OnDestroy {


  public ngIdSegmentsMap: Record<string, number[]> = {}

  private sliceviewLoading$: Observable<boolean>

  public overrideShowLayers: string[] = []

  public lifecycle: INehubaLifecycleHook

  public viewerPosInVoxel$ = new BehaviorSubject(null)
  public viewerPosInReal$ = new BehaviorSubject(null)
  public mousePosInVoxel$ = new BehaviorSubject(null)
  public mousePosInReal$ = new BehaviorSubject(null)

  private exportNehuba: any

  private subscriptions: Subscription[] = []

  @Output() public nehubaReady: EventEmitter<null> = new EventEmitter()
  @Output() public layersChanged: EventEmitter<null> = new EventEmitter()
  private layersChangedHandler: any
  @Output() public viewerPositionChange: EventEmitter<any> = new EventEmitter()
  @Output() public mouseoverSegmentEmitter:
    EventEmitter<{
      segmentId: number | null
      layer: {
        name?: string
        url?: string
      }
    }> = new EventEmitter()
  @Output() public mouseoverLandmarkEmitter: EventEmitter<string> = new EventEmitter()
  @Output() public mouseoverUserlandmarkEmitter: EventEmitter<string> = new EventEmitter()
  @Output() public regionSelectionEmitter: EventEmitter<{
    segment: number,
    layer: {
      name?: string,
      url?: string
  }}> = new EventEmitter()
  @Output() public errorEmitter: EventEmitter<any> = new EventEmitter()


  /* only used to set initial navigation state */
  public initNav: any
  public initRegions: any[]
  public initNiftiLayers: any[] = []

  public config: any
  public nehubaViewer: any
  private _dim: [number, number, number]
  get dim() {
    return this._dim
      ? this._dim
      : [1.5e9, 1.5e9, 1.5e9]
  }

  public _s2$: any = null
  public _s3$: any = null
  public _s4$: any = null
  public _s5$: any = null
  public _s6$: any = null
  public _s7$: any = null
  public _s8$: any = null

  public _s$: any[] = [
    this._s2$,
    this._s3$,
    this._s4$,
    this._s5$,
    this._s6$,
    this._s7$,
    this._s8$,
  ]

  public ondestroySubscriptions: Subscription[] = []

  private createNehubaPromiseRs: () => void
  private createNehubaPromise = new Promise<void>(rs => {
    this.createNehubaPromiseRs = rs
  })

  public nehubaLoaded: boolean = false

  public landmarksLoaded: boolean = false

  constructor(
    public elementRef: ElementRef,
    private workerService: AtlasWorkerService,
    private log: LoggingService,
    @Inject(IMPORT_NEHUBA_INJECT_TOKEN) getImportNehubaPr: () => Promise<any>,
    @Optional() @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Subject<NehubaViewerUnit>,
    @Optional() @Inject(SET_MESHES_TO_LOAD) private injSetMeshesToLoad$: Observable<IMeshesToLoad>,
    @Optional() @Inject(SET_COLORMAP_OBS) private setColormap$: Observable<IColorMap>,
    @Optional() @Inject(SET_LAYER_VISIBILITY) private layerVis$: Observable<string[]>,
    @Optional() @Inject(SET_SEGMENT_VISIBILITY) private segVis$: Observable<string[]>,
    @Optional() @Inject(NG_LAYER_CONTROL) private layerCtrl$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>>,
  ) {

    if (this.nehubaViewer$) {
      this.nehubaViewer$.next(this)
    }

    getImportNehubaPr()
      .then(() => {
        this.nehubaLoaded = true
        this.exportNehuba = getExportNehuba()
        const fixedZoomPerspectiveSlices = this.config && this.config.layout && this.config.layout.useNehubaPerspective && this.config.layout.useNehubaPerspective.fixedZoomPerspectiveSlices
        if (fixedZoomPerspectiveSlices) {
          const { sliceZoom, sliceViewportWidth, sliceViewportHeight } = fixedZoomPerspectiveSlices
          const dim = Math.min(sliceZoom * sliceViewportWidth, sliceZoom * sliceViewportHeight)
          this._dim = [dim, dim, dim]
        }
        this.patchNG()
        this.loadNehuba()

        this.layersChangedHandler = this.nehubaViewer.ngviewer.layerManager.layersChanged.add(() => this.layersChanged.emit(null))
        this.nehubaViewer.ngviewer.registerDisposer(this.layersChangedHandler)
      })
      .then(() => {
        // all mutation to this.nehubaViewer should await createNehubaPromise
        this.createNehubaPromiseRs()
      })
      .catch(e => this.errorEmitter.emit(e))


    /**
     * TODO move to layerCtrl.service
     */
    this.ondestroySubscriptions.push(
      fromEvent(this.workerService.worker, 'message').pipe(
        filter((message: any) => {

          if (!message) {
            // this.log.error('worker response message is undefined', message)
            return false
          }
          if (!message.data) {
            // this.log.error('worker response message.data is undefined', message.data)
            return false
          }
          if (message.data.type !== 'ASSEMBLED_USERLANDMARKS_VTK') {
            /* worker responded with not assembled landmark, no need to act */
            return false
          }
          /**
           * nb url may be undefined
           * if undefined, user have removed all user landmarks, and all that needs to be done
           * is remove the user landmark layer
           *
           * message.data.url
           */

          return true
        }),
        debounceTime(100),
        map(e => e.data.url),
      ).subscribe(url => {
        this.landmarksLoaded = !!url
        this.removeuserLandmarks()

        /**
         * url may be null if user removes all landmarks
         */
        if (!url) {
          /**
           * remove transparency from meshes in current layer(s)
           */
          this.setMeshTransparency(false)
          return
        }
        const _ = {}
        _[NG_USER_LANDMARK_LAYER_NAME] = {
          type: 'mesh',
          source: `vtk://${url}`,
          shader: this.userLandmarkShader,
        }
        this.loadLayer(_)

        /**
         * adding transparency to meshes in current layer(s)
         */
        this.setMeshTransparency(true)
      }),
    )
  
    if (this.setColormap$) {
      this.ondestroySubscriptions.push(
        this.setColormap$.pipe(
          switchMap(switchMapWaitFor({
            condition: () => !!(this.nehubaViewer?.ngviewer)
          })),
          debounceTime(160),
        ).subscribe(v => {
          const map = new Map()
          for (const key in v) {
            const m = new Map()
            map.set(key, m)
            for (const lblIdx in v[key]) {
              m.set(lblIdx, v[key][lblIdx])
            }
          }
          this.setColorMap(map)
        })
      )
    } else {
      this.log.error(`SET_COLORMAP_OBS not provided`)
    }

    if (this.layerVis$) {
      this.ondestroySubscriptions.push(
        this.layerVis$.pipe(
          switchMap(switchMapWaitFor({ condition: () => !!(this.nehubaViewer?.ngviewer) })),
          distinctUntilChanged(arrayOrderedEql),
          debounceTime(160),
        ).subscribe((layerNames: string[]) => {
          /**
           * debounce 160ms to set layer invisible etc
           * on switch from freesurfer -> volumetric viewer, race con results in managed layer not necessarily setting layer visible correctly
           */
          const managedLayers = this.nehubaViewer.ngviewer.layerManager.managedLayers
          managedLayers.forEach(layer => layer.setVisible(false))
          
          for (const layerName of layerNames) {
            const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
            if (layer) {
              layer.setVisible(true)
            } else {
              this.log.log('layer unavailable', layerName)
            }
          }
        })
      )
    } else {
      this.log.error(`SET_LAYER_VISIBILITY not provided`)
    }

    if (this.segVis$) {
      this.ondestroySubscriptions.push(
        this.segVis$.pipe(
          switchMap(
            switchMapWaitFor({
              condition: () => this.nehubaViewer?.ngviewer,
              leading: true,
            })
          )
        ).subscribe(val => {
          console.log(val)
          // null === hide all seg
          if (val === null) {
            this.hideAllSeg()
            return
          }
          // empty array === show all seg
          if (val.length === 0) {
            this.showAllSeg()
            return
          }
          // show limited seg
          this.showSegs(val)
        })
      )
    } else {
      this.log.error(`SET_SEGMENT_VISIBILITY not provided`)
    }

    if (this.layerCtrl$) {
      this.ondestroySubscriptions.push(
        this.layerCtrl$.pipe(
          bufferUntil(({
            condition: () => !!this.nehubaViewer?.ngviewer
          }))
        ).subscribe(messages => {
          for (const message of messages) {
            if (message.type === 'add') {
              const p = message as TNgLayerCtrl<'add'>
              this.loadLayer(p.payload)
            }
            if (message.type === 'remove') {
              const p = message as TNgLayerCtrl<'remove'>
              for (const name of p.payload.names){
                this.removeLayer({ name })
              }
            }
            if (message.type === 'update') {
              const p = message as TNgLayerCtrl<'update'>
              this.updateLayer(p.payload)
            }
            if (message.type === 'setLayerTransparency') {
              const p = message as TNgLayerCtrl<'setLayerTransparency'>
              for (const key in p.payload) {
                this.setLayerTransparency(key, p.payload[key])
              }
            }
          }
        })
      )
    } else {
      this.log.error(`NG_LAYER_CONTROL not provided`)
    }
  }

  public numMeshesToBeLoaded: number = 0

  public applyPerformanceConfig({ gpuLimit }: Partial<ViewerConfiguration>) {
    if (gpuLimit && this.nehubaViewer) {
      const limit = this.nehubaViewer.ngviewer.state.children.get('gpuMemoryLimit')
      if (limit && limit.restoreState) {
        limit.restoreState(gpuLimit)
      }
    }
  }

  public spatialLandmarkSelectionChanged(labels: number[]) {
    const getCondition = (label: number) => `if(label > ${label - 0.1} && label < ${label + 0.1} ){${FRAGMENT_EMIT_RED}}`
    const newShader = `void main(){ ${labels.map(getCondition).join('else ')}else {${FRAGMENT_EMIT_WHITE}} }`
    if (!this.nehubaViewer) {
      this.log.warn('setting special landmark selection changed failed ... nehubaViewer is not yet defined')
      return
    }
    const landmarkLayer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(NG_LANDMARK_LAYER_NAME)
    if (!landmarkLayer) {
      this.log.warn('landmark layer could not be found ... will not update colour map')
      return
    }
    if (labels.length === 0) {
      landmarkLayer.layer.displayState.fragmentMain.restoreState(FRAGMENT_MAIN_WHITE)
    } else {
      landmarkLayer.layer.displayState.fragmentMain.restoreState(newShader)
    }
  }

  public navPosReal: [number, number, number] = [0, 0, 0]
  public navPosVoxel: [number, number, number] = [0, 0, 0]

  public mousePosReal: [number, number, number] = [0, 0, 0]
  public mousePosVoxel: [number, number, number] = [0, 0, 0]

  public viewerState: ViewerState

  private _multiNgIdColorMap: Map<string, Map<number, {red: number, green: number, blue: number}>>
  get multiNgIdColorMap() {
    return this._multiNgIdColorMap
  }

  set multiNgIdColorMap(val) {
    this._multiNgIdColorMap = val
  }

  private loadMeshes$: ReplaySubject<{labelIndicies: number[], layer: { name: string }}> = new ReplaySubject()

  public mouseOverSegment: number | null
  public mouseOverLayer: {name: string, url: string}| null

  public getNgHash: () => string = () => this.exportNehuba
    ? this.exportNehuba.getNgHash()
    : null

  public redraw() {
    this.nehubaViewer.redraw()
  }

  public loadNehuba() {
    this.nehubaViewer = this.exportNehuba.createNehubaViewer(this.config, (err) => {
      /* print in debug mode */
      this.log.error(err)
    })

    /**
     * Hide all layers except the base layer (template)
     * Then show the layers referenced in multiNgIdLabelIndexMap
     */

    this.redraw()

    /* creation of the layout is done on next frame, hence the settimeout */
    setTimeout(() => {
      getViewer().display.panels.forEach(patchSliceViewPanel)
      this.nehubaReady.emit(null)
    })

    this.newViewerInit()
    this.loadNewParcellation()

    setNehubaViewer(this.nehubaViewer)

    this.onDestroyCb.push(() => setNehubaViewer(null))
  }

  public ngOnInit() {
    this.sliceviewLoading$ = fromEvent(this.elementRef.nativeElement, 'sliceRenderEvent').pipe(
      scan(scanSliceViewRenderFn, [ null, null, null ]),
      map(arrOfFlags => arrOfFlags.some(flag => flag)),
      startWith(true),
    )

    this.subscriptions.push(
      (this.injSetMeshesToLoad$ || this.loadMeshes$).pipe(
        scan(scanFn, []),
        debounceTime(100),
        switchMap(layerLabelIdx => 
          /**
           * sometimes (e.g. when all slice views are minimised), sliceviewlaoding will not emit
           * so if sliceviewloading does not emit another value (except the initial true value)
           * force start loading of mesh
           */
          race(
            this.sliceviewLoading$.pipe(
              skip(1)
            ),
            timer(500).pipe(
              mapTo(false)
            )
          ).pipe(
            filter(flag => !flag),
            take(1),
            mapTo(layerLabelIdx),
          ) 
        ),
      ).subscribe(layersLabelIndex => {
        let totalMeshes = 0
        for (const layerLayerIndex of layersLabelIndex) {
          const { layer, labelIndicies } = layerLayerIndex
          totalMeshes += labelIndicies.length
          this.nehubaViewer.setMeshesToLoad(labelIndicies, layer)
        }
        // TODO implement total mesh to be loaded and mesh loading UI
        this.numMeshesToBeLoaded = totalMeshes
      }),
    )

    const { onInit } = this.lifecycle || {}
    onInit && onInit.call(this)
  }

  public ngOnDestroy() {
    if (this.nehubaViewer$) {
      this.nehubaViewer$.next(null)
    }
    while (this.subscriptions.length > 0) {
      this.subscriptions.pop().unsubscribe()
    }

    this._s$.forEach(_s$ => {
      if (_s$) { _s$.unsubscribe() }
    })
    this.ondestroySubscriptions.forEach(s => s.unsubscribe())
    while (this.onDestroyCb.length > 0) {
      this.onDestroyCb.pop()()
    }
    this.nehubaViewer && this.nehubaViewer.dispose()
  }

  private onDestroyCb: Array<() => void> = []

  private patchNG() {

    const { LayerManager, UrlHashBinding } = this.exportNehuba.getNgPatchableObj()

    UrlHashBinding.prototype.setUrlHash = () => {
      // this.log.log('seturl hash')
      // this.log.log('setting url hash')
    }

    UrlHashBinding.prototype.updateFromUrlHash = () => {
      // this.log.log('update hash binding')
    }

    /* TODO find a more permanent fix to disable double click */
    LayerManager.prototype.invokeAction = (arg) => {

      /**
       * The emitted value does not affect the region selection
       * the region selection is taken care of in nehubaContainer
       */
      
      if (arg === 'select') {
        this.regionSelectionEmitter.emit({
          segment: this.mouseOverSegment,
          layer: this.mouseOverLayer
        })
      }
    }

    /* eslint-disable-next-line @typescript-eslint/no-empty-function */
    this.onDestroyCb.push(() => LayerManager.prototype.invokeAction = (_arg) => { /** in default neuroglancer, this function is invoked when selection occurs */ })
  }

  private filterLayers(l: any, layerObj: any): boolean {
    /**
     * if selector is an empty object, select all layers
     */
    return layerObj instanceof Object && Object.keys(layerObj).every(key =>
      /**
       * the property described by the selector must exist and ...
       */
      !!l[key] &&
        /**
         * if the selector is regex, test layer property
         */
        ( layerObj[key] instanceof RegExp
          ? layerObj[key].test(l[key])
          /**
           * if selector is string, test for strict equality
           */
          : typeof layerObj[key] === 'string'
            ? layerObj[key] === l[key]
            /**
             * otherwise do not filter
             */
            : false
        ),
    )
  }

  private userLandmarkShader: string = FRAGMENT_MAIN_WHITE
  
  // TODO single landmark for user landmark
  public updateUserLandmarks(landmarks: any[]) {
    if (!this.nehubaViewer) {
      return
    }
    
    this.workerService.worker.postMessage({
      type : 'GET_USERLANDMARKS_VTK',
      scale: Math.min(...this.dim.map(v => v * NG_LANDMARK_CONSTANT)),
      landmarks : landmarks.map(lm => lm.position.map(coord => coord * 1e6)),
    })

    const parseLmColor = lm => {
      if (!lm) return null
      const { color } = lm
      if (!color) return null
      if (!Array.isArray(color)) return null
      if (color.length !== 3) return null
      const parseNum = num => (num >= 0 && num <= 255 ? num / 255 : 1).toFixed(3)
      return `emitRGB(vec3(${color.map(parseNum).join(',')}));`
    }
  
    const appendConditional = (frag, idx) => frag && `if (label > ${idx - 0.01} && label < ${idx + 0.01}) { ${frag} }`

    if (landmarks.some(parseLmColor)) {
      this.userLandmarkShader = `void main(){ ${landmarks.map(parseLmColor).map(appendConditional).filter(v => !!v).join('else ')} else {${FRAGMENT_EMIT_WHITE}} }`
    } else {
      this.userLandmarkShader = FRAGMENT_MAIN_WHITE  
    }
  }

  public removeSpatialSearch3DLandmarks() {
    this.removeLayer({
      name : NG_LANDMARK_LAYER_NAME,
    })
  }

  public removeuserLandmarks() {
    this.removeLayer({
      name : NG_USER_LANDMARK_LAYER_NAME,
    })
  }

  public setLayerVisibility(condition: {name: string|RegExp}, visible: boolean) {
    if (!this.nehubaViewer) {
      return false
    }
    const viewer = this.nehubaViewer.ngviewer
    viewer.layerManager.managedLayers
      .filter(l => this.filterLayers(l, condition))
      .map(layer => layer.setVisible(visible))
  }

  public removeLayer(layerObj: any) {
    if (!this.nehubaViewer) {
      return false
    }
    const viewer = this.nehubaViewer.ngviewer
    const removeLayer = (i) => (viewer.layerManager.removeManagedLayer(i), i.name)

    return viewer.layerManager.managedLayers
      .filter(l => this.filterLayers(l, layerObj))
      .map(removeLayer)
  }

  public loadLayer(layerObj: any) {
    const viewer = this.nehubaViewer.ngviewer
    return Object.keys(layerObj)
      .filter(key =>
        /* if the layer exists, it will not be loaded */
        !viewer.layerManager.getLayerByName(key))
      .map(key => {
        viewer.layerManager.addManagedLayer(
          viewer.layerSpecification.getLayer(key, layerObj[key]))

        return layerObj[key]
      })
  }

  public updateLayer(layerObj: INgLayerCtrl['update']) {

    const viewer = this.nehubaViewer.ngviewer

    for (const layerName in layerObj) {
      const layer = viewer.layerManager.getLayerByName(layerName)
      if (!layer) continue
      const { visible } = layerObj[layerName]
      layer.setVisible(!!visible)
    }
  }

  public hideAllSeg() {
    if (!this.nehubaViewer) return
    for (const ngId in this.ngIdSegmentsMap) {
      for (const idx of this.ngIdSegmentsMap[ngId]) {
        this.nehubaViewer.hideSegment(idx, {
          name: ngId,
        })
      }

      this.nehubaViewer.showSegment(0, {
        name: ngId,
      })
    }
  }

  public showAllSeg() {
    if (!this.nehubaViewer) { return }
    for (const ngId in this.ngIdSegmentsMap) {
      console.log(ngId)
      for (const idx of this.ngIdSegmentsMap[ngId]) {
        this.nehubaViewer.showSegment(idx, {
          name: ngId,
        })
      }

      this.nehubaViewer.hideSegment(0, {
        name: ngId,
      })
    }
  }

  public showSegs(array: (number|string)[]) {

    if (!this.nehubaViewer) { return }

    this.hideAllSeg()

    if (array.length === 0) { return }

    /**
     * TODO tobe deprecated
     */

    if (typeof array[0] === 'number') {
      this.log.warn(`show seg with number indices has been deprecated`)
      return
    }

    const reduceFn: (acc: Map<string, number[]>, curr: string) => Map<string, number[]> = (acc, curr) => {

      const newMap = new Map(acc)
      const { ngId, label: labelIndex } = deserializeSegment(curr)
      const exist = newMap.get(ngId)
      if (!exist) {
        newMap.set(ngId, [Number(labelIndex)])
      } else {
        newMap.set(ngId, [...exist, Number(labelIndex)])
      }
      return newMap
    }

    const newMap: Map<string, number[]> = array.reduce(reduceFn, new Map())

    /**
     * TODO
     * ugh, ugly code. cleanify
     */
    /**
     * TODO
     * sometimes, ngId still happends to be undefined
     */
    newMap.forEach((segs, ngId) => {
      this.nehubaViewer.hideSegment(0, {
        name: ngId,
      })
      segs.forEach(seg => {
        this.nehubaViewer.showSegment(seg, {
          name: ngId,
        })
      })
    })
  }

  private vec3(pos: number[]) {
    return this.exportNehuba.vec3.fromValues(...pos)
  }

  public setNavigationState(newViewerState: Partial<ViewerState>) {

    if (!this.nehubaViewer) {
      this.log.warn('setNavigationState > this.nehubaViewer is not yet defined')
      return
    }

    const {
      orientation,
      perspectiveOrientation,
      perspectiveZoom,
      position,
      positionReal,
      zoom,
    } = newViewerState

    if ( perspectiveZoom ) {
      this.nehubaViewer.ngviewer.perspectiveNavigationState.zoomFactor.restoreState(perspectiveZoom)
    }
    if ( zoom ) {
      this.nehubaViewer.ngviewer.navigationState.zoomFactor.restoreState(zoom)
    }
    if ( perspectiveOrientation ) {
      this.nehubaViewer.ngviewer.perspectiveNavigationState.pose.orientation.restoreState( perspectiveOrientation )
    }
    if ( orientation ) {
      this.nehubaViewer.ngviewer.navigationState.pose.orientation.restoreState( orientation )
    }
    if ( position ) {
      this.nehubaViewer.setPosition( this.vec3(position) , positionReal ? true : false )
    }
  }

  public obliqueRotateX(amount: number) {
    this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 1, 0]), -amount / 4.0 * Math.PI / 180.0)
  }

  public obliqueRotateY(amount: number) {
    this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([1, 0, 0]), amount / 4.0 * Math.PI / 180.0)
  }

  public obliqueRotateZ(amount: number) {
    this.nehubaViewer.ngviewer.navigationState.pose.rotateRelative(this.vec3([0, 0, 1]), amount / 4.0 * Math.PI / 180.0)
  }

  public toggleOctantRemoval(flag?: boolean) {
    const ctrl = this.nehubaViewer?.ngviewer?.showPerspectiveSliceViews
    if (!ctrl) {
      this.log.error(`toggleOctantRemoval failed. this.nehubaViewer.ngviewer?.showPerspectiveSliceViews returns falsy`)
      return
    }
    const newVal = typeof flag === 'undefined'
      ? !ctrl.value
      : flag
    ctrl.restoreState(newVal)

    if (this.landmarksLoaded) {
      /**
       * showPerspectSliceView -> ! meshTransparency
       */
      this.setMeshTransparency(!newVal)
    }
  }

  private setLayerTransparency(layerName: string, alpha: number) {
    const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
    if (!layer) return
    layer.layer.displayState.objectAlpha.restoreState(alpha)
  }

  public setMeshTransparency(flag: boolean){

    /**
     * remove transparency from meshes in current layer(s)
     */
    for (const layerKey in this.ngIdSegmentsMap) {
      const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerKey)
      if (layer) {
        layer.layer.displayState.objectAlpha.restoreState(flag ? 0.2 : 1.0)
      }
    }
  }

  private newViewerInit() {

    /* isn't this layer specific? */
    /* TODO this is layer specific. need a way to distinguish between different segmentation layers */
    this._s2$ = this.nehubaViewer.mouseOver.segment
      .subscribe(({ segment, layer }) => {
        this.mouseOverSegment = segment
        this.mouseOverLayer = { ...layer }
      })

    if (this.initNav) {
      this.setNavigationState(this.initNav)
      this.initNav = null
    }

    if (this.initRegions && this.initRegions.length > 0) {
      this.hideAllSeg()
      this.showSegs(this.initRegions)
    }

    if (this.initNiftiLayers.length > 0) {
      this.initNiftiLayers.forEach(layer => this.loadLayer(layer))
      this.hideAllSeg()
    }

    this._s8$ = this.nehubaViewer.mouseOver.segment.subscribe(({segment: segmentId, layer }) => {
      this.mouseoverSegmentEmitter.emit({
        layer,
        segmentId,
      })
    })

    // nehubaViewer.navigationState.all emits every time a new layer is added or removed from the viewer
    this._s3$ = this.nehubaViewer.navigationState.all
      .distinctUntilChanged((a, b) => {
        const {
          orientation: o1,
          perspectiveOrientation: po1,
          perspectiveZoom: pz1,
          position: p1,
          zoom: z1,
        } = a
        const {
          orientation: o2,
          perspectiveOrientation: po2,
          perspectiveZoom: pz2,
          position: p2,
          zoom: z2,
        } = b

        return [0, 1, 2, 3].every(idx => o1[idx] === o2[idx]) &&
          [0, 1, 2, 3].every(idx => po1[idx] === po2[idx]) &&
          pz1 === pz2 &&
          [0, 1, 2].every(idx => p1[idx] === p2[idx]) &&
          z1 === z2
      })
      .filter(() => !this.initNav)
      .subscribe(({ orientation, perspectiveOrientation, perspectiveZoom, position, zoom }) => {
        this.viewerState = {
          orientation,
          perspectiveOrientation,
          perspectiveZoom,
          zoom,
          position,
          positionReal : false,
        }

        this.viewerPositionChange.emit({
          orientation : Array.from(orientation),
          perspectiveOrientation : Array.from(perspectiveOrientation),
          perspectiveZoom,
          zoom,
          position: Array.from(position),
          positionReal : true,
        })
      })

    // TODO bug: mouseoverlandmarkemitter does not emit empty for VTK layer when user mouse click
    this.ondestroySubscriptions.push(
      this.nehubaViewer.mouseOver.layer
        .filter(obj => obj.layer.name === NG_LANDMARK_LAYER_NAME)
        .subscribe(obj => this.mouseoverLandmarkEmitter.emit(obj.value)),
    )

    this.ondestroySubscriptions.push(
      this.nehubaViewer.mouseOver.layer
        .filter(obj => obj.layer.name === NG_USER_LANDMARK_LAYER_NAME)
        .subscribe(obj => this.mouseoverUserlandmarkEmitter.emit(obj.value)),
    )

    this._s4$ = this.nehubaViewer.navigationState.position.inRealSpace
      .filter(v => typeof v !== 'undefined' && v !== null)
      .subscribe(v => {
        this.navPosReal = Array.from(v) as [number, number, number]
        this.viewerPosInReal$.next(Array.from(v))
      })
    this._s5$ = this.nehubaViewer.navigationState.position.inVoxels
      .filter(v => typeof v !== 'undefined' && v !== null)
      .subscribe(v => {
        this.navPosVoxel = Array.from(v) as [number, number, number]
        this.viewerPosInVoxel$.next(Array.from(v))
      })
    this._s6$ = this.nehubaViewer.mousePosition.inRealSpace
      .filter(v => typeof v !== 'undefined' && v !== null)
      .subscribe(v => {
        this.mousePosReal = Array.from(v) as [number, number, number]
        this.mousePosInReal$.next(Array.from(v))
      })
    this._s7$ = this.nehubaViewer.mousePosition.inVoxels
      .filter(v => typeof v !== 'undefined' && v !== null)
      .subscribe(v => {
        this.mousePosVoxel = Array.from(v) as [number, number, number]
        this.mousePosInVoxel$.next(Array.from(v))
      })
  }

  private loadNewParcellation() {

    this._s$.forEach(_s$ => {
      if (_s$) { _s$.unsubscribe() }
    })

  }

  private setColorMap(map: Map<string, Map<number, {red: number, green: number, blue: number}>>) {
    this.multiNgIdColorMap = map
    for (const [ ngId, cMap ] of map.entries()) {
      const nMap = new Map()
      for (const [ key, cm ] of cMap.entries()) {
        nMap.set(Number(key), cm)
      }
      this.nehubaViewer.batchAddAndUpdateSegmentColors(
        nMap,
        { name : ngId })
    }
  }
}

const patchSliceViewPanel = (sliceViewPanel: any) => {
  const originalDraw = sliceViewPanel.draw
  sliceViewPanel.draw = function(this) {

    if (this.sliceView) {
      const viewportToDataEv = new CustomEvent('viewportToData', {
        bubbles: true,
        detail: {
          viewportToData : this.sliceView.viewportToData,
        },
      })
      this.element.dispatchEvent(viewportToDataEv)
    }

    originalDraw.call(this)
  }
}

export interface ViewerState {
  orientation: number[]
  perspectiveOrientation: number[]
  perspectiveZoom: number
  position: number[]
  positionReal: boolean
  zoom: number
}

export const ICOSAHEDRON = `# vtk DataFile Version 2.0
Converted using https://github.com/HumanBrainProject/neuroglancer-scripts
ASCII
DATASET POLYDATA
POINTS 12 float
-525731.0 0.0 850651.0
525731.0 0.0 850651.0
-525731.0 0.0 -850651.0
525731.0 0.0 -850651.0
0.0 850651.0 525731.0
0.0 850651.0 -525731.0
0.0 -850651.0 525731.0
0.0 -850651.0 -525731.0
850651.0 525731.0 0.0
-850651.0 525731.0 0.0
850651.0 -525731.0 0.0
-850651.0 -525731.0 0.0
POLYGONS 20 80
3 1 4 0
3 4 9 0
3 4 5 9
3 8 5 4
3 1 8 4
3 1 10 8
3 10 3 8
3 8 3 5
3 3 2 5
3 3 7 2
3 3 10 7
3 10 6 7
3 6 11 7
3 6 0 11
3 6 1 0
3 10 1 6
3 11 0 9
3 2 11 9
3 5 2 9
3 11 2 7`

declare const TextEncoder

export const _encoder = new TextEncoder()
export const ICOSAHEDRON_VTK_URL = URL.createObjectURL( new Blob([ _encoder.encode(ICOSAHEDRON) ], {type : 'application/octet-stream'} ))

export const FRAGMENT_MAIN_WHITE = `void main(){emitRGB(vec3(1.0,1.0,1.0));}`
export const FRAGMENT_EMIT_WHITE = `emitRGB(vec3(1.0, 1.0, 1.0));`
export const FRAGMENT_EMIT_RED = `emitRGB(vec3(1.0, 0.1, 0.12));`
export const computeDistance = (pt1: [number, number], pt2: [number, number]) => ((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2) ** 0.5