import { Component, Inject, ViewChild, ChangeDetectionStrategy, inject, HostListener } from "@angular/core";
import { FormControl } from "@angular/forms";
import { select, Store } from "@ngrx/store";
import { BehaviorSubject, combineLatest, concat, merge, NEVER, Observable, of, Subject } from "rxjs";
import { switchMap, distinctUntilChanged, map, debounceTime, shareReplay, take, withLatestFrom, filter, takeUntil, tap } from "rxjs/operators";
import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"
import { selectedTemplate } from "src/state/atlasSelection/selectors";
import { panelMode, panelOrder } from "src/state/userInterface/selectors";
import { ResizeObserverDirective } from "src/util/windowResize";
import { NehubaViewerUnit } from "../../nehubaViewer/nehubaViewer.component";
import { EnumPanelMode } from "../../store/store";
import { NEHUBA_INSTANCE_INJTKN } from "../../util";
import { EnumClassicalView } from "src/atlasComponents/constants"
import { atlasSelection } from "src/state";
import { floatEquality } from "common/util"
import { CURRENT_TEMPLATE_DIM_INFO, TemplateInfo } from "../../layerCtrl.service/layerCtrl.util";
import { DestroyDirective } from "src/util/directives/destroy.directive";
import { isNullish, isWheelEvent, switchMapWaitFor } from "src/util/fn"

const MAX_DIM = 200

type AnatomicalOrientation = 'ap' | 'si' | 'rl' // anterior-posterior, superior-inferior, right-left
type RangeOrientation = 'horizontal' | 'vertical'
const anatOriToIdx: Record<AnatomicalOrientation, number> = {
  'rl': 0,
  'ap': 1,
  'si': 2
}
const anaOriAltAxis: Record<AnatomicalOrientation, (templateInfo: TemplateInfo, ratio: {x: number, y: number}) => {idx: number, value: number}> = {
  'rl': (templateInfo, { y }) => ({ idx: 2, value: templateInfo.real[0] * (0.5 - y) }),
  'ap': (templateInfo, { y }) => ({ idx: 2, value: templateInfo.real[1] * (0.5 - y) }),
  'si': (templateInfo, { y }) => ({ idx: 0, value: templateInfo.real[2] * (y - 0.5) })
}

function getDim(triplet: number[], view: EnumClassicalView) {
  if (view === EnumClassicalView.AXIAL) {
    return [triplet[0], triplet[1]]
  }
  if (view === EnumClassicalView.CORONAL) {
    return [triplet[0], triplet[2]]
  }
  if (view === EnumClassicalView.SAGITTAL) {
    return [triplet[1], triplet[2]]
  }
}

type ModArr = {
  idx: number
  value: number
}

@Component({
  selector: 'nehuba-perspective-view-slider',
  templateUrl: './perspectiveViewSlider.template.html',
  styleUrls: ['./perspectiveViewSlider.style.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  hostDirectives: [
    DestroyDirective,
  ]
})

export class PerspectiveViewSlider {

    #xr = new BehaviorSubject(null)
    #yr = new BehaviorSubject(null)
    #xyRatio = combineLatest([
      this.#xr.pipe(distinctUntilChanged()),
      this.#yr.pipe(distinctUntilChanged()),
    ]).pipe(
      map(([ x, y ]) => ({ x, y }))
    )
    mousemove(ev: MouseEvent){
      this.#mousemove.next(ev)
      const target = (ev.target as HTMLInputElement)
      this.#xr.next(ev.offsetX / target.clientWidth)
      this.#yr.next(ev.offsetY / target.clientHeight)
    }

    #mousemove = new Subject()
    #mousedown = new Subject()
    #mouseup = new Subject()
    #dragging = this.#mousedown.pipe(
      switchMap(() => this.#mousemove.pipe(
        takeUntil(this.#mouseup)
      ))
    )
    mousedown(){
      this.#mousedown.next(true)
    }

    @HostListener('document:mouseup')
    mouseup(){
      this.#mouseup.next(true)
    }

    #zoom = new Subject<number>()
    mousewheel(ev: Event){
      if (!isWheelEvent(ev)) {
        return
      }
      this.#zoom.next(ev.deltaY)
    }

    #destroy$ = inject(DestroyDirective).destroyed$

    @ViewChild(ResizeObserverDirective)
    resizeDirective: ResizeObserverDirective

    public minimapControl = new FormControl<number>(0)
    public recalcViewportSize$ = new Subject()

    private selectedTemplate$ = this.store$.pipe(
      select(selectedTemplate),
      distinctUntilChanged((o, n) => o?.id === n?.id),
    )
    private maximisedPanelIndex$ = combineLatest([
      this.store$.pipe(
        select(panelMode),
        distinctUntilChanged(),
      ),
      this.store$.pipe(
        select(panelOrder),
        distinctUntilChanged(),
      ),
    ]).pipe(
      map(([ mode, order ]) => {
        // TODO order can potentially be nullish
        if (!([EnumPanelMode.PIP_PANEL, EnumPanelMode.SINGLE_PANEL].includes(mode as EnumPanelMode))) {
          return null
        }
        return Number(order[0])
      })
    )

    private viewportSize$ = concat(
      of(null), // emit on init
      this.recalcViewportSize$,
    ).pipe(
      debounceTime(160),
      map(() => {
        const panel = document.getElementsByClassName('neuroglancer-panel') as HTMLCollectionOf<HTMLElement>
        if (!(panel?.[0])) {
          return null
        }
        return {
          width: panel[0].offsetWidth,
          height: panel[0].offsetHeight
        }
      }),
      shareReplay(1),
    )

    private navPosition$: Observable<{real: [number, number, number], voxel: [number, number, number]}> = this.nehubaViewer$.pipe(
      switchMap(viewer => {
        if (!viewer) return of(null)
        return combineLatest([
          viewer.viewerPosInReal$,
          viewer.viewerPosInVoxel$,
        ]).pipe(
          map(([ real, voxel ]) => {
            return { real, voxel }
          })
        )
      }),
      shareReplay(1)
    )

    private rangeControlSetting$ = this.maximisedPanelIndex$.pipe(
      map(maximisedPanelIndex => {
        let anatomicalOrientation: AnatomicalOrientation = null
        let rangeOrientation: RangeOrientation = null
        let minimapView: EnumClassicalView
        let sliceView: EnumClassicalView
        if (maximisedPanelIndex === 0) {
          anatomicalOrientation = 'ap'
          rangeOrientation = 'horizontal'
          minimapView = EnumClassicalView.SAGITTAL
          sliceView = EnumClassicalView.CORONAL
        }
        if (maximisedPanelIndex === 1) {
          anatomicalOrientation = 'rl'
          rangeOrientation = 'horizontal'
          minimapView = EnumClassicalView.CORONAL
          sliceView = EnumClassicalView.SAGITTAL
        }
        if (maximisedPanelIndex === 2) {
          anatomicalOrientation = 'si'
          rangeOrientation = 'vertical'
          minimapView = EnumClassicalView.CORONAL
          sliceView = EnumClassicalView.AXIAL
        }
        return {
          anatomicalOrientation,
          rangeOrientation,
          minimapView,
          sliceView
        }
      })
    )

    public rangeControlIsVertical$ = this.rangeControlSetting$.pipe(
      map(ctrl => ctrl?.rangeOrientation === "vertical")
    )

    private currentTemplateSize$ = this.tmplInfo$.pipe(
      filter(val => !!val)
    )

    private useMinimap$: Observable<EnumClassicalView> = this.maximisedPanelIndex$.pipe(
      map(maximisedPanelIndex => {
        if (maximisedPanelIndex === 0) return EnumClassicalView.SAGITTAL
        if (maximisedPanelIndex === 1) return EnumClassicalView.CORONAL
        if (maximisedPanelIndex === 2) return EnumClassicalView.CORONAL
        return null
      })
    )


    // this crazy hack is required since firefox support vertical-orient
    // do not and -webkit-slider-thumb#apperance cannot be used to hide the thumb
    public rangeInputStyle$ = this.rangeControlIsVertical$.pipe(
      withLatestFrom(this.currentTemplateSize$, this.useMinimap$),
      map(([ isVertical, templateSizes, useMinimap ]) => {
        if (!isVertical) return {}
        const { real } = templateSizes
        const [ width, height ] = getDim(real, useMinimap)
        const max = Math.max(width, height)
        const useHeight = width/max*MAX_DIM
        const useWidth = height/max*MAX_DIM

        const xformOriginVal = Math.min(useHeight, useWidth)/2
        const transformOrigin = `${xformOriginVal}px ${xformOriginVal}px`

        return {
          height: `${useHeight}px`,
          width: `${useWidth}px`,
          transformOrigin,
        }
      })
    )

    public rangeControlMinMaxValue$ = this.currentTemplateSize$.pipe(
      switchMap(templateSize => {
        return this.rangeControlSetting$.pipe(
          switchMap(orientation => this.navPosition$.pipe(
            switchMap(
              switchMapWaitFor({
                condition: nav => !!nav && !!nav.real
              })
            ),
            take(1),
            map(nav => {
              if (!nav || !orientation || !templateSize) return null
              
              const { real: realPos } = nav

              const { anatomicalOrientation: anatOri } = orientation
              const idx = anatOriToIdx[anatOri]
              
              const { real, transform } = templateSize
              if (!transform || !transform[idx]) return null
              const min = Math.round(transform[idx][3])
              const max = Math.round(real[idx] + transform[idx][3])

              return {
                min, max, value: realPos[idx]
              }
            })
          ))
        )
      }),
    )
  
    public previewImageUrl$ = combineLatest([
      this.selectedTemplate$,
      this.useMinimap$,
      this.navPosition$,
    ]).pipe(
      map(([template, view, nav]) => {
        let useImgIdx = 0
        if (view === EnumClassicalView.SAGITTAL) {
          const { real } = nav || {}
          const xPos = real?.[0] || 0
          useImgIdx = xPos < 0 ? 0 : 1
        }
        const url = getScreenshotUrl(template, view, useImgIdx)
        if (!url) return null
        return `assets/images/persp-view/${url}`
      }),
      distinctUntilChanged()
    )

    public sliceviewIsNormal$ = this.store$.pipe(
      select(atlasSelection.selectors.navigation),
      map(navigation => {
        // if navigation in store is nullish, assume starting position, ie slice view is normal
        if (!navigation) return true
        return [0, 0, 0, 1].every((v, idx) => floatEquality(navigation.orientation[idx], v,  1e-3))})
    )

    public textToDisplay$ = combineLatest([
      this.sliceviewIsNormal$,
      this.navPosition$,
      this.maximisedPanelIndex$,
    ]).pipe(
      map(([ sliceviewIsNormal, nav, maximisedIdx ]) => {
        if (!sliceviewIsNormal) return null
        if (!(nav?.real) || (maximisedIdx === null)) return null
        return `${(nav.real[maximisedIdx === 0? 1 : maximisedIdx === 1? 0 : 2] / 1e6).toFixed(3)}mm`
      })
    )

    public scrubberPosition$ = this.rangeControlMinMaxValue$.pipe(
      switchMap(minmaxval => concat(
        of(null as number),
        this.minimapControl.valueChanges,
      ).pipe(
        map(newval => {
          if (!minmaxval) return null
          const { min, max, value } = minmaxval
          if (min === null || max === null) return null
          const useValue = newval ?? value
          if (useValue === null) return null
          const translate = 100 * (useValue - min) / (max - min)
          return `translateX(${translate}%)`
        })
      ))
    )

    public scrubberHighlighter$ = this.nehubaViewer$.pipe(
      switchMap(viewer => combineLatest([
        // on component init, the viewerPositionChange would not have fired
        // in this case. So we get the zoom from the store as the first value
        concat(
          this.store$.pipe(
            select(atlasSelection.selectors.navigation),
            take(1)
          ),
          viewer
            ? viewer.viewerPositionChange
            : NEVER,
        ),
        this.viewportSize$,
        this.rangeControlSetting$,
        this.currentTemplateSize$,
        this.rangeControlIsVertical$,
      ]).pipe(
        map(([ navigation, viewportSize, ctrl, templateSize, ..._rest ]) => {
          if (!ctrl || !(templateSize?.real) || !navigation) return null

          const { zoom, position } = navigation

          let translate: number = null
          const { sliceView } = ctrl

          const getTranslatePc = (idx: number) => {
            const trueCenter = templateSize.real[idx] / 2
            const compensate = trueCenter + templateSize.transform[idx][3]
            return (position[idx] - compensate) / templateSize.real[idx]
          }

          let scale: number = 2
          const sliceviewDim = getDim(templateSize.real, sliceView)
          if (!sliceviewDim) return null

          if (sliceView === EnumClassicalView.CORONAL) {
            // minimap is sagittal view, so interested in superior-inferior axis
            translate = getTranslatePc(2)
            scale = Math.min(scale, viewportSize.height * zoom / sliceviewDim[1])
          }

          if (sliceView === EnumClassicalView.SAGITTAL) {
            // minimap is coronal view, so interested in superior-inferior axis
            translate = getTranslatePc(2)
            scale = Math.min(scale, viewportSize.height * zoom / sliceviewDim[1])
          }

          if (sliceView === EnumClassicalView.AXIAL) {
            // minimap  is in coronal view, so interested in left-right axis
            translate = getTranslatePc(0) * -1
            scale = Math.min(scale, viewportSize.width * zoom / sliceviewDim[0])
          }

          /**
           * calculate scale
           */
          const scaleString = `scaleY(${scale})`

          /**
           * calculate translation
           */
          const translateString = `translateY(${translate * -100}%)`

          return `${translateString} ${scaleString}`
        })
      ))
    )

    constructor(
      private store$: Store,
      @Inject(NEHUBA_INSTANCE_INJTKN) private nehubaViewer$: Observable<NehubaViewerUnit>,
      @Inject(CURRENT_TEMPLATE_DIM_INFO) private tmplInfo$: Observable<TemplateInfo>,
    ) {

      const posMod$ = this.rangeControlSetting$.pipe(
        switchMap(rangeCtrl => this.#dragging.pipe(
          withLatestFrom(
            this.minimapControl.valueChanges,
            this.currentTemplateSize$,
            this.#xyRatio,
          ),
          map(([_, newValue, currTmplSize, xyRatio]) => {
            
            const positionMod: ModArr[] = []

            const { anatomicalOrientation } = rangeCtrl
            if (!isNullish(anatomicalOrientation) && !isNullish(newValue)) {
              const idx = anatOriToIdx[anatomicalOrientation]
              positionMod.push({
                idx,
                value: newValue
              })
              
            }

            if (!isNullish(xyRatio.x) && !isNullish(xyRatio.y)) {
              const { idx, value } = anaOriAltAxis[anatomicalOrientation](currTmplSize, xyRatio)
              positionMod.push({
                idx,
                value
              })
            }
            return { positionMod, zoom: null as number }
          })
        ))
      )

      const zoom$ = this.nehubaViewer$.pipe(
        switchMap(nehubaViewer => this.#zoom.pipe(
          withLatestFrom(nehubaViewer
          ? nehubaViewer.viewerPositionChange
          : NEVER),
          map(([zoom, posChange]) => {
            const { zoom: currZoom } = posChange
            return {
              zoom: zoom > 0 ? currZoom * 1.2 : currZoom * 0.8,
              positionMod: null as ModArr[]
            }
          })
        ))
      )

      this.nehubaViewer$.pipe(
        switchMap(nehubaViewer =>
          merge(
            posMod$,
            zoom$,
          ).pipe(
            map(({ positionMod, zoom }) => ({
              nehubaViewer, positionMod, zoom
            }))
          )
        ),
        withLatestFrom(
          this.navPosition$.pipe(
            map(value => value?.real)
          ),
        ),
        takeUntil(this.#destroy$)
      ).subscribe(([{ nehubaViewer, positionMod, zoom }, currentPosition]) => {

        const newNavPosition = [...currentPosition]
        if (!isNullish(positionMod)) {
          for (const { idx, value } of positionMod) {
            newNavPosition[idx] = value
          }
        }
        nehubaViewer.setNavigationState({
          position: newNavPosition,
          ...(isNullish(zoom) ? {} : { zoom }),
          positionReal: true
        })
      })

      combineLatest([
        this.sliceviewIsNormal$,
        this.navPosition$,
        this.maximisedPanelIndex$,
      ]).pipe(
        filter(([ sliceViewIsNormal ]) => sliceViewIsNormal),
        map(([ _, ...rest ]) => rest),
        takeUntil(this.#destroy$)
      ).subscribe(([ navPos, maximisedIdx]) => {
        const realPos = navPos?.real
        if (!realPos) {
          return
        }
        const pos = navPos.real[maximisedIdx === 0? 1 : maximisedIdx === 1? 0 : 2]
        const diff = Math.abs(this.minimapControl.value - pos)
        if (diff > 1e6) {
          this.minimapControl.setValue(pos)
        }
      })
    }


    resetSliceview() {
      this.store$.dispatch(
        atlasSelection.actions.navigateTo({
          animation: true,
          navigation: {
            orientation: [0, 0, 0, 1]
          }
        })
      )
    }
  
}

const spaceIdToPrefix = {
  "minds/core/referencespace/v1.0.0/dafcffc5-4826-4bf1-8ff6-46b8a31ff8e2": "mni152",
  "minds/core/referencespace/v1.0.0/7f39f7be-445b-47c0-9791-e971c0b6d992": "colin27",
  "minds/core/referencespace/v1.0.0/a1655b99-82f1-420f-a3c2-fe80fd4c8588": "bigbrain",
  "minds/core/referencespace/v1.0.0/MEBRAINS": "mebrains",
  "minds/core/referencespace/v1.0.0/265d32a0-3d84-40a5-926f-bf89f68212b9": "allen",
  "minds/core/referencespace/v1.0.0/d5717c4a-0fa1-46e6-918c-b8003069ade8": "waxholm"
}

const viewToSuffix = {
  [EnumClassicalView.SAGITTAL]: 'sagittal',
  [EnumClassicalView.AXIAL]: 'axial',
  [EnumClassicalView.CORONAL]: 'coronal',
}

function getScreenshotUrl(space: SxplrTemplate, requestedView: EnumClassicalView, imgIdx: number = 0): string {
  const prefix = spaceIdToPrefix[space?.id]
  if (!prefix) return null
  const suffix = viewToSuffix[requestedView]
  if (!suffix) return null
  return `${prefix}_${suffix}_${imgIdx}.png`
}