/** @jsx jsx */
import { jsx } from '@emotion/core';
import styled from '@emotion/styled';
import { useCallback, useRef, useEffect, useMemo, useContext } from 'react';
import { closedToolbarWidth, expandedTrackHeight, trackMargin, trackInnerPadding, navHeight, openToolbarWidth } from '../../styles/dimensions';
import Track from '../Track';
import { ProjectCoords, SnapMode } from '../../types';
import { viewportPxToSamples, getContentWidth, clampScrollLeft } from './calculator';
import useKeyCommand, { Key, Modifier } from '../../hooks/useKeyCommand';
import { ProjectContext } from '../../hooks/useProject';
import { getSelectionStart, getSelectedTracks, getSelectionEnd } from '../../audio/utils/selectionTools';
import { TimelineViewControllerContext } from '../../hooks/useTimelineViewController';
import { ScreenConfigurationContext } from '../../hooks/useScreenConfiguration';
import { PlayerContext } from '../../hooks/useProjectPlayer';
import useAnimationFrame from '../../hooks/useAnimationFrame';
import Playhead from './Playhead';

const TrackListWrapper = styled.div`
  position: relative;
  overflow-y: scroll;
  min-height: 100%;
  ::-webkit-scrollbar {
    display: none;
  }
`;

const TrackList = styled.ol`
  list-style-type: none;
  min-height: 100%;
  margin: 0;
  padding: 0;
`;

const MouseCatcher = styled.div`
  position: fixed;
  top: ${navHeight}px;
  overflow: scroll;
  ::-webkit-scrollbar {
    display: none;
  }
`;

const MouseCatcherContent = styled.div`
  min-width: 100%;
  min-height: 100%;
`;

export default () => {
  const {
    interactions,
    state: projectState,
    selection
  } = useContext(ProjectContext);
  const player = useContext(PlayerContext);
  const { duration, paused, fastGetCurrentTimeRef } = player;

  const { getCombinedState, state: timelineState, interactions: timelineViewInteractions } = useContext(TimelineViewControllerContext);
  const { follow } = timelineState;
  const { setScrollLeft, setFollow } = timelineViewInteractions;
  const wasPaused = useRef(paused);

  useEffect(
    () => {
      wasPaused.current = paused;
    },
    [paused]
  );

  const trackListHeight = useMemo(() => projectState.tracks.reduce((a, track) => a + track.height, 0), [projectState.tracks]);

  const mouseCatcherRef = useRef<HTMLDivElement>(null);
  const trackListWrapperRef = useRef<HTMLDivElement>(null);
  const trackListRef = useRef<HTMLOListElement>(null);

  useKeyCommand(Key.Dash, timelineViewInteractions.zoomOut);
  useKeyCommand(Key.Equals, timelineViewInteractions.zoomIn);
  useKeyCommand([Modifier.Ctrl, Key.Num0], timelineViewInteractions.showAll);

  useKeyCommand(
    Key.F,
    useCallback(
      () => timelineViewInteractions.showPoint(fastGetCurrentTimeRef.current()),
      [timelineViewInteractions, fastGetCurrentTimeRef]
    )
  );

  useKeyCommand(
    [Modifier.Shift, Key.F],
    useCallback(
      () => setFollow(!follow),
      [follow, setFollow]
    )
  );

  // set to true before performing a user scroll to prevent an update loop.
  const scrollUpdateBlockerRef = useRef(false);

  const reflectScrollLeftCallback = useCallback(
    () => {
      const mouseCatcher = mouseCatcherRef.current;
      if (mouseCatcher && !scrollUpdateBlockerRef.current) {
        mouseCatcher.scrollLeft = getCombinedState().scrollLeft;
      }
      scrollUpdateBlockerRef.current = false;
    },
    [getCombinedState]
  );

  useAnimationFrame(reflectScrollLeftCallback, paused || !follow);

  const catchTimelineScroll = useCallback(
    (e) => {
      scrollUpdateBlockerRef.current = true;
      setScrollLeft(clampScrollLeft(getCombinedState(), e.currentTarget.scrollLeft));
      if (trackListWrapperRef.current) {
        trackListWrapperRef.current.scrollTop = e.currentTarget.scrollTop;
      }
    },
    [getCombinedState, trackListWrapperRef, setScrollLeft]
  );

  const screenConfiguration = useContext(ScreenConfigurationContext);

  const timelineLeft = screenConfiguration.toolbarOpen ? openToolbarWidth : closedToolbarWidth;

  const getMouseEventCoordinates = useCallback(
    (e) => {
      const mouseCatcherElement = mouseCatcherRef.current;
      if (!mouseCatcherElement) {
        return null;
      }
      const y = (e.clientY + mouseCatcherElement.scrollTop) - navHeight;
      let trackIndex = Infinity;
      let currentTop = 0;
      for (let i = 0; i < projectState.tracks.length; i ++) {
        currentTop += projectState.tracks[i].height + trackMargin;
        if (y < currentTop) {
          trackIndex = i;
          break;
        }
      }

      const positionWithinTrack = y % (expandedTrackHeight + trackMargin);
      if (positionWithinTrack <= trackMargin) return null;

      return [
        Math.min(
          timelineState.duration,
          Math.max(
            0,
            Math.round(viewportPxToSamples(getCombinedState(), e.clientX - timelineLeft))
          ),
        ),
        trackIndex > projectState.tracks.length - 1 ? null : trackIndex
      ] as ProjectCoords;
    },
    [projectState.tracks, timelineState, getCombinedState, timelineLeft]
  );

  const onMouseEnd = useCallback(() => {
    interactions.selection.end();
    player.seek(getSelectionStart(selection), true);
  }, [interactions.selection, selection, player]);

  useEffect(
    () => {
      const handleMouseMove = (e: MouseEvent) => {
        const snapMode = e.altKey ? SnapMode.Off : SnapMode.Zeroes;
        interactions.selection.continue(getMouseEventCoordinates(e), snapMode);
      };
      window.addEventListener('mousemove', handleMouseMove);
      return () => window.removeEventListener('mousemove', handleMouseMove);
    },
    [getMouseEventCoordinates, interactions.selection]
  );

  const mouseEndRef = useRef(onMouseEnd);

  useEffect(() => {
    mouseEndRef.current = onMouseEnd;
  }, [onMouseEnd])

  const catchTimelineMouseDown = useCallback(
    (e) => {
      const snapMode = e.altKey ? SnapMode.Off : SnapMode.Zeroes;
      interactions.selection.start(getMouseEventCoordinates(e), snapMode);
      const endSelection = () => {
        mouseEndRef.current();
        window.removeEventListener('mouseup', endSelection);
        document.body.className = "";
      }
      window.addEventListener('mouseup', endSelection);
      document.body.className = "no-select";
    },
    [getMouseEventCoordinates, interactions.selection]
  );

  // hack for efficiency
  const contentWidth = useMemo(
    () => Math.ceil(getContentWidth({
      duration,
      samplesPerPx: getCombinedState().samplesPerPx,
      padding: trackInnerPadding,
      scrollLeft: 0,
      viewportWidth: 0
    })),
    [duration, getCombinedState]
  );

  const runFollow = useCallback(
    () => {
      timelineViewInteractions.showPoint(fastGetCurrentTimeRef.current());

    },
    [timelineViewInteractions, fastGetCurrentTimeRef]
  );

  const lastTracks = useRef(projectState.tracks);
  useEffect(() => {
    const listWrapper = trackListWrapperRef.current;
    const list = trackListRef.current;
    if (!listWrapper || !list) return;

    if (
      lastTracks.current.length > 0 &&
      projectState.tracks.length > lastTracks.current.length &&
      !lastTracks.current.find((t, i) => t !== projectState.tracks[i])
    ) {
      listWrapper.scrollTop = list.getBoundingClientRect().height - listWrapper.getBoundingClientRect().height
    }
    lastTracks.current = projectState.tracks;
  }, [projectState.tracks])

  useAnimationFrame(runFollow, timelineState.follow && !paused);

  return (
    <TrackListWrapper ref={trackListWrapperRef} style={{ width: `${getCombinedState().viewportWidth}px` }}>
      <TrackList ref={trackListRef}>
        {projectState.tracks.map(({ audioBuffers, title, height }, index) => {
          const selected = getSelectedTracks(selection).includes(index);
          return (
            <Track
              key={index}
              index={index}
              analyser={player.analysers[index]}
              title={title}
              audioBuffers={audioBuffers}
              height={height}
              resize={interactions.track.resize}
              rename={interactions.track.rename}
              deleteTrack={interactions.track.deleteTrack}
              selectionStart={selected ? getSelectionStart(selection) : null}
              selectionEnd={selected ? getSelectionEnd(selection) : null}
            />
          );
        })}
      </TrackList>

      <MouseCatcher
        ref={mouseCatcherRef}
        onScroll={catchTimelineScroll}
        onMouseDown={catchTimelineMouseDown}
        style={{
          width: `${timelineState.viewportWidth}px`,
          left: `${timelineLeft}px`,
          height: `calc(${100 - screenConfiguration.commandViewHeight}vh - 27px)`,
        }}
      >
        <MouseCatcherContent style={{
          width: `${contentWidth}px`,
          height: `${trackListHeight}px`
        }} />
      </MouseCatcher>

      <Playhead />
    </TrackListWrapper>
  )
}
