/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { useSetting, useMeasure } from '@web/uiUtils';
import { msToString } from '@isomorphic/formatUtils';
import { GlassPane } from '@web/shared/glassPane';
import * as React from 'react';
import type { Boundaries } from './geometry';
import { FilmStrip } from './filmStrip';
import type { FilmStripPreviewPoint } from './filmStrip';
import type { ActionTraceEventInContext, TraceModel } from '@isomorphic/trace/traceModel';
import './timeline.css';
import type { Language } from '@isomorphic/locatorGenerators';
import type { ActionGroup } from '@isomorphic/protocolFormatter';

export const Timeline: React.FunctionComponent<{
  model: TraceModel | undefined,
  boundaries: Boundaries,
  onSelected: (action: ActionTraceEventInContext) => void,
  selectedTime: Boundaries | undefined,
  setSelectedTime: (time: Boundaries | undefined) => void,
  sdkLanguage: Language,
  scrubber?: React.ReactNode,
}> = ({ model, boundaries, onSelected, selectedTime, setSelectedTime, sdkLanguage, scrubber }) => {
  const [measure, ref] = useMeasure<HTMLDivElement>();
  const [dragWindow, setDragWindow] = React.useState<{ startX: number, endX: number, pivot?: number, type: 'resize' | 'move' } | undefined>();
  const [previewPoint, setPreviewPoint] = React.useState<FilmStripPreviewPoint | undefined>();
  const [actionsFilter] = useSetting<ActionGroup[]>('actionsFilter', []);

  const { offsets, curtainLeft, curtainRight } = React.useMemo(() => {
    let activeWindow = selectedTime || boundaries;
    if (dragWindow && dragWindow.startX !== dragWindow.endX) {
      const time1 = positionToTime(measure.width, boundaries, dragWindow.startX);
      const time2 = positionToTime(measure.width, boundaries, dragWindow.endX);
      activeWindow = { minimum: Math.min(time1, time2), maximum: Math.max(time1, time2) };
    }
    const curtainLeft = timeToPosition(measure.width, boundaries, activeWindow.minimum);
    const maxRight = timeToPosition(measure.width, boundaries, boundaries.maximum);
    const curtainRight = maxRight - timeToPosition(measure.width, boundaries, activeWindow.maximum);
    return { offsets: calculateDividerOffsets(measure.width, boundaries), curtainLeft, curtainRight };
  }, [selectedTime, boundaries, dragWindow, measure]);

  const actions = React.useMemo(() => model?.filteredActions(actionsFilter), [model, actionsFilter]);

  const onMouseDown = React.useCallback((event: React.MouseEvent) => {
    setPreviewPoint(undefined);
    if (!ref.current)
      return;
    const x = event.clientX - ref.current.getBoundingClientRect().left;
    const time = positionToTime(measure.width, boundaries, x);
    const leftX = selectedTime ? timeToPosition(measure.width, boundaries, selectedTime.minimum) : 0;
    const rightX = selectedTime ? timeToPosition(measure.width, boundaries, selectedTime.maximum) : 0;

    if (selectedTime && Math.abs(x - leftX) < 10) {
      // Resize left.
      setDragWindow({ startX: rightX, endX: x, type: 'resize' });
    } else if (selectedTime && Math.abs(x - rightX) < 10) {
      // Resize right.
      setDragWindow({ startX: leftX, endX: x, type: 'resize' });
    } else if (selectedTime && time > selectedTime.minimum && time < selectedTime.maximum && event.clientY - ref.current.getBoundingClientRect().top < 20) {
      // Move window.
      setDragWindow({ startX: leftX, endX: rightX, pivot: x, type: 'move' });
    } else {
      // Create new.
      setDragWindow({ startX: x, endX: x, type: 'resize' });
    }
  }, [boundaries, measure, ref, selectedTime]);

  const onGlassPaneMouseMove = React.useCallback((event: MouseEvent) => {
    if (!ref.current)
      return;
    const x = event.clientX - ref.current.getBoundingClientRect().left;
    const time = positionToTime(measure.width, boundaries, x);
    const action = actions?.findLast(action => action.startTime <= time);

    if (!event.buttons) {
      setDragWindow(undefined);
      return;
    }

    // When moving window reveal action under cursor.
    if (action)
      onSelected(action);

    // Should not happen, but for type safety.
    if (!dragWindow)
      return;

    let newDragWindow = dragWindow;
    if (dragWindow.type === 'resize') {
      newDragWindow = { ...dragWindow, endX: x };
    } else {
      const delta = x - dragWindow.pivot!;
      let startX = dragWindow.startX + delta;
      let endX = dragWindow.endX + delta;
      if (startX < 0) {
        startX = 0;
        endX = startX + (dragWindow.endX - dragWindow.startX);
      }
      if (endX > measure.width) {
        endX = measure.width;
        startX = endX - (dragWindow.endX - dragWindow.startX);
      }
      newDragWindow = { ...dragWindow, startX, endX, pivot: x };
    }

    setDragWindow(newDragWindow);
    const time1 = positionToTime(measure.width, boundaries, newDragWindow.startX);
    const time2 = positionToTime(measure.width, boundaries, newDragWindow.endX);
    if (time1 !== time2)
      setSelectedTime({ minimum: Math.min(time1, time2), maximum: Math.max(time1, time2) });
  }, [boundaries, dragWindow, measure, actions, onSelected, ref, setSelectedTime]);

  const onGlassPaneMouseUp = React.useCallback(() => {
    setPreviewPoint(undefined);
    if (!dragWindow)
      return;
    if (dragWindow.startX !== dragWindow.endX) {
      const time1 = positionToTime(measure.width, boundaries, dragWindow.startX);
      const time2 = positionToTime(measure.width, boundaries, dragWindow.endX);
      setSelectedTime({ minimum: Math.min(time1, time2), maximum: Math.max(time1, time2) });
    } else {
      const time = positionToTime(measure.width, boundaries, dragWindow.startX);
      const action = actions?.findLast(action => action.startTime <= time);
      if (action)
        onSelected(action);
      setSelectedTime(undefined);
    }
    setDragWindow(undefined);
  }, [boundaries, dragWindow, measure, actions, setSelectedTime, onSelected]);

  const onMouseMove = React.useCallback((event: React.MouseEvent) => {
    if (!ref.current)
      return;
    const x = event.clientX - ref.current.getBoundingClientRect().left;
    const time = positionToTime(measure.width, boundaries, x);
    const action = actions?.findLast(action => action.startTime <= time);
    setPreviewPoint({ x, clientY: event.clientY, action, sdkLanguage });
  }, [boundaries, measure, actions, ref, sdkLanguage]);

  const onMouseLeave = React.useCallback(() => {
    setPreviewPoint(undefined);
  }, []);

  const onPaneDoubleClick = React.useCallback(() => {
    setSelectedTime(undefined);
  }, [setSelectedTime]);

  return <div className='timeline-view-container'>
    {!!dragWindow && <GlassPane
      cursor={dragWindow?.type === 'resize' ? 'ew-resize' : 'grab'}
      onPaneMouseUp={onGlassPaneMouseUp}
      onPaneMouseMove={onGlassPaneMouseMove}
      onPaneDoubleClick={onPaneDoubleClick} />}
    <div ref={ref}
      className='timeline-view'
      onMouseDown={onMouseDown}
      onMouseMove={onMouseMove}
      onMouseLeave={onMouseLeave}>
      <div className='timeline-grid'>{
        offsets.map((offset, index) => {
          return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
            <div className='timeline-time'>{msToString(offset.time - boundaries.minimum)}</div>
          </div>;
        })
      }</div>
      <FilmStrip boundaries={boundaries} previewPoint={previewPoint} />
      {scrubber}
      {selectedTime && <div className='timeline-window'>
        <div className='timeline-window-curtain left' style={{ width: curtainLeft }}></div>
        <div className='timeline-window-resizer' style={{ left: -5 }}></div>
        <div className='timeline-window-center'>
          <div className='timeline-window-drag'></div>
        </div>
        <div className='timeline-window-resizer' style={{ left: 5 }}></div>
        <div className='timeline-window-curtain right' style={{ width: curtainRight }}></div>
      </div>}
    </div>
  </div>;
};

function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { position: number, time: number }[] {
  const minimumGap = 64;
  let dividerCount = clientWidth / minimumGap;
  const boundarySpan = boundaries.maximum - boundaries.minimum;
  const pixelsPerMillisecond = clientWidth / boundarySpan;
  let sectionTime = boundarySpan / dividerCount;

  const logSectionTime = Math.ceil(Math.log(sectionTime) / Math.LN10);
  sectionTime = Math.pow(10, logSectionTime);
  if (sectionTime * pixelsPerMillisecond >= 5 * minimumGap)
    sectionTime = sectionTime / 5;
  if (sectionTime * pixelsPerMillisecond >= 2 * minimumGap)
    sectionTime = sectionTime / 2;

  const firstDividerTime = boundaries.minimum;
  let lastDividerTime = boundaries.maximum;
  lastDividerTime += minimumGap / pixelsPerMillisecond;
  dividerCount = Math.ceil((lastDividerTime - firstDividerTime) / sectionTime);

  if (!sectionTime)
    dividerCount = 0;

  const offsets = [];
  for (let i = 0; i < dividerCount; ++i) {
    const time = firstDividerTime + sectionTime * i;
    offsets.push({ position: timeToPosition(clientWidth, boundaries, time), time });
  }
  return offsets;
}

function timeToPosition(clientWidth: number, boundaries: Boundaries, time: number): number {
  return (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth;
}

function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number {
  return x / clientWidth * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
}
