import React, { memo, useEffect, useState, useCallback, useRef, Fragment } from 'react';
import { IoIosHelpCircle, IoIosCamera } from 'react-icons/io';
import { FaPlus, FaMinus, FaHandPointRight, FaHandPointLeft, FaTrashAlt, FaVolumeMute, FaVolumeUp, FaYinYang, FaFileExport } from 'react-icons/fa';
import { MdRotate90DegreesCcw, MdCallSplit, MdCallMerge } from 'react-icons/md';
import { FiScissors } from 'react-icons/fi';
import { AnimatePresence, motion } from 'framer-motion';
import Swal from 'sweetalert2';
import Lottie from 'react-lottie';
import { SideSheet, Button, Position } from 'evergreen-ui';
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
import fromPairs from 'lodash/fromPairs';
import clamp from 'lodash/clamp';
import cloneDeep from 'lodash/cloneDeep';
import sortBy from 'lodash/sortBy';
import flatMap from 'lodash/flatMap';
import HelpSheet from './HelpSheet';
import TimelineSeg from './TimelineSeg';
import InverseCutSegment from './InverseCutSegment';
import StreamsSelector from './StreamsSelector';
import { loadMifiLink } from './mifi';
import loadingLottie from './7077-magic-flow.json';
const isDev = require('electron-is-dev');
const electron = require('electron'); // eslint-disable-line
const Mousetrap = require('mousetrap');
const Hammer = require('react-hammerjs').default;
const { dirname } = require('path');
const trash = require('trash');
const uuid = require('uuid');
const ReactDOM = require('react-dom');
const { default: PQueue } = require('p-queue');
const { unlink, exists } = require('fs-extra');
const { showMergeDialog, showOpenAndMergeDialog } = require('./merge/merge');
const allOutFormats = require('./outFormats');
const captureFrame = require('./capture-frame');
const ffmpeg = require('./ffmpeg');
const configStore = require('./store');
const { defaultProcessedCodecTypes, getStreamFps, isCuttingStart, isCuttingEnd } = ffmpeg;
const {
  getOutPath, parseDuration, formatDuration, toast, errorToast, showFfmpegFail, setFileNameTitle,
  promptTimeOffset, generateColor, getOutDir,
} = require('./util');
const { dialog } = electron.remote;
function withBlur(cb) {
  return (e) => {
    cb(e);
    e.target.blur();
  };
}
function createSegment({ start, end } = {}) {
  return {
    start,
    end,
    color: generateColor(),
    uuid: uuid.v4(),
  };
}
const dragPreventer = ev => {
  ev.preventDefault();
};
function doesPlayerSupportFile(streams) {
  // TODO improve, whitelist supported codecs instead
  return !streams.find(s => ['hevc', 'prores'].includes(s.codec_name));
  // return true;
}
const queue = new PQueue({ concurrency: 1 });
const App = memo(() => {
  // Per project state
  const [framePath, setFramePath] = useState();
  const [html5FriendlyPath, setHtml5FriendlyPath] = useState();
  const [working, setWorking] = useState(false);
  const [dummyVideoPath, setDummyVideoPath] = useState(false);
  const [playing, setPlaying] = useState(false);
  const [playerTime, setPlayerTime] = useState();
  const [duration, setDuration] = useState();
  const [fileFormat, setFileFormat] = useState();
  const [detectedFileFormat, setDetectedFileFormat] = useState();
  const [rotation, setRotation] = useState(360);
  const [cutProgress, setCutProgress] = useState();
  const [startTimeOffset, setStartTimeOffset] = useState(0);
  const [rotationPreviewRequested, setRotationPreviewRequested] = useState(false);
  const [filePath, setFilePath] = useState('');
  const [externalStreamFiles, setExternalStreamFiles] = useState([]);
  const [detectedFps, setDetectedFps] = useState();
  const [mainStreams, setStreams] = useState([]);
  const [copyStreamIdsByFile, setCopyStreamIdsByFile] = useState({});
  const [streamsSelectorShown, setStreamsSelectorShown] = useState(false);
  const [zoom, setZoom] = useState(1);
  const [commandedTime, setCommandedTime] = useState(0);
  const [ffmpegCommandLog, setFfmpegCommandLog] = useState([]);
  // Segment related state
  const [currentSegIndex, setCurrentSegIndex] = useState(0);
  const [cutStartTimeManual, setCutStartTimeManual] = useState();
  const [cutEndTimeManual, setCutEndTimeManual] = useState();
  const [cutSegments, setCutSegments, cutSegmentsHistory] = useStateWithHistory(
    [createSegment()],
    100,
  );
  // Preferences
  const [captureFormat, setCaptureFormat] = useState(configStore.get('captureFormat'));
  useEffect(() => configStore.set('captureFormat', captureFormat), [captureFormat]);
  const [customOutDir, setCustomOutDir] = useState(configStore.get('customOutDir'));
  useEffect(() => (customOutDir === undefined ? configStore.delete('customOutDir') : configStore.set('customOutDir', customOutDir)), [customOutDir]);
  const [keyframeCut, setKeyframeCut] = useState(configStore.get('keyframeCut'));
  useEffect(() => configStore.set('keyframeCut', keyframeCut), [keyframeCut]);
  const [autoMerge, setAutoMerge] = useState(configStore.get('autoMerge'));
  useEffect(() => configStore.set('autoMerge', autoMerge), [autoMerge]);
  const [timecodeShowFrames, setTimecodeShowFrames] = useState(configStore.get('timecodeShowFrames'));
  useEffect(() => configStore.set('timecodeShowFrames', timecodeShowFrames), [timecodeShowFrames]);
  const [invertCutSegments, setInvertCutSegments] = useState(configStore.get('invertCutSegments'));
  useEffect(() => configStore.set('invertCutSegments', invertCutSegments), [invertCutSegments]);
  const [autoExportExtraStreams, setAutoExportExtraStreams] = useState(configStore.get('autoExportExtraStreams'));
  useEffect(() => configStore.set('autoExportExtraStreams', autoExportExtraStreams), [autoExportExtraStreams]);
  const [askBeforeClose, setAskBeforeClose] = useState(configStore.get('askBeforeClose'));
  useEffect(() => configStore.set('askBeforeClose', askBeforeClose), [askBeforeClose]);
  const [muted, setMuted] = useState(configStore.get('muted'));
  useEffect(() => configStore.set('muted', muted), [muted]);
  // Global state
  const [helpVisible, setHelpVisible] = useState(false);
  const [mifiLink, setMifiLink] = useState();
  const videoRef = useRef();
  const timelineWrapperRef = useRef();
  const timelineScrollerRef = useRef();
  const timelineScrollerSkipEventRef = useRef();
  function appendFfmpegCommandLog(command) {
    setFfmpegCommandLog(old => [...old, command]);
  }
  function setCopyStreamIdsForPath(path, cb) {
    setCopyStreamIdsByFile((old) => {
      const oldIds = old[path] || {};
      return ({ ...old, [path]: cb(oldIds) });
    });
  }
  function toggleCopyStreamId(path, index) {
    setCopyStreamIdsForPath(path, (old) => ({ ...old, [index]: !old[index] }));
  }
  function toggleMute() {
    setMuted((v) => {
      if (!v) toast.fire({ title: 'Muted preview (note that exported file will not be affected)' });
      return !v;
    });
  }
  function seekAbs(val) {
    const video = videoRef.current;
    if (val == null || Number.isNaN(val)) return;
    let outVal = val;
    if (outVal < 0) outVal = 0;
    if (outVal > video.duration) outVal = video.duration;
    video.currentTime = outVal;
    setCommandedTime(outVal);
  }
  const seekRel = useCallback((val) => {
    seekAbs(videoRef.current.currentTime + val);
  }, []);
  const shortStep = useCallback((dir) => {
    seekRel((1 / 60) * dir);
  }, [seekRel]);
  const resetState = useCallback(() => {
    const video = videoRef.current;
    setCommandedTime(0);
    video.currentTime = 0;
    video.playbackRate = 1;
    setFileNameTitle();
    setFramePath();
    setHtml5FriendlyPath();
    setDummyVideoPath();
    setWorking(false);
    setPlaying(false);
    setDuration();
    cutSegmentsHistory.go(0);
    setCutSegments([createSegment()]); // TODO this will cause two history items
    setCutStartTimeManual();
    setCutEndTimeManual();
    setFileFormat();
    setDetectedFileFormat();
    setRotation(360);
    setCutProgress();
    setStartTimeOffset(0);
    setRotationPreviewRequested(false);
    setFilePath(''); // Setting video src="" prevents memory leak in chromium
    setExternalStreamFiles([]);
    setDetectedFps();
    setStreams([]);
    setCopyStreamIdsByFile({});
    setStreamsSelectorShown(false);
    setZoom(1);
  }, [cutSegmentsHistory, setCutSegments]);
  useEffect(() => () => {
    if (dummyVideoPath) unlink(dummyVideoPath).catch(console.error);
  }, [dummyVideoPath]);
  const frameRenderEnabled = !!(rotationPreviewRequested || dummyVideoPath);
  // Because segments could have undefined start / end
  // (meaning extend to start of timeline or end duration)
  function getSegApparentStart(seg) {
    const time = seg.start;
    return time !== undefined ? time : 0;
  }
  const getSegApparentEnd = useCallback((seg) => {
    const time = seg.end;
    if (time !== undefined) return time;
    if (duration !== undefined) return duration;
    return 0; // Haven't gotten duration yet
  }, [duration]);
  const apparentCutSegments = cutSegments.map(cutSegment => ({
    ...cutSegment,
    start: getSegApparentStart(cutSegment),
    end: getSegApparentEnd(cutSegment),
  }));
  const invalidSegUuids = apparentCutSegments
    .filter(cutSegment => cutSegment.start >= cutSegment.end)
    .map(cutSegment => cutSegment.uuid);
  const haveInvalidSegs = invalidSegUuids.length > 0;
  const currentSegIndexSafe = Math.min(currentSegIndex, cutSegments.length - 1);
  const currentCutSeg = cutSegments[currentSegIndexSafe];
  const currentApparentCutSeg = apparentCutSegments[currentSegIndexSafe];
  const areWeCutting = apparentCutSegments.length > 1
    || isCuttingStart(currentApparentCutSeg.start)
    || isCuttingEnd(currentApparentCutSeg.end, duration);
  const inverseCutSegments = (() => {
    if (haveInvalidSegs) return undefined;
    if (apparentCutSegments.length < 1) return undefined;
    const sorted = sortBy(apparentCutSegments, 'start');
    const foundOverlap = sorted.some((cutSegment, i) => {
      if (i === 0) return false;
      return sorted[i - 1].end > cutSegment.start;
    });
    if (foundOverlap) return undefined;
    if (duration == null) return undefined;
    const ret = [];
    if (sorted[0].start > 0) {
      ret.push({
        start: 0,
        end: sorted[0].start,
      });
    }
    sorted.forEach((cutSegment, i) => {
      if (i === 0) return;
      ret.push({
        start: sorted[i - 1].end,
        end: cutSegment.start,
      });
    });
    const last = sorted[sorted.length - 1];
    if (last.end < duration) {
      ret.push({
        start: last.end,
        end: duration,
      });
    }
    return ret;
  })();
  const setCutTime = useCallback((type, time) => {
    const cloned = cloneDeep(cutSegments);
    const currentSeg = currentCutSeg;
    if (type === 'start' && time >= getSegApparentEnd(currentSeg)) {
      throw new Error('Start time must precede end time');
    }
    if (type === 'end' && time <= getSegApparentStart(currentSeg)) {
      throw new Error('Start time must precede end time');
    }
    cloned[currentSegIndexSafe][type] = Math.min(Math.max(time, 0), duration);
    setCutSegments(cloned);
  }, [
    currentSegIndexSafe, getSegApparentEnd, cutSegments, currentCutSeg, setCutSegments, duration,
  ]);
  function formatTimecode(sec) {
    return formatDuration({ seconds: sec, fps: timecodeShowFrames ? detectedFps : undefined });
  }
  const getCurrentTime = useCallback(() => (
    playing ? playerTime : commandedTime), [commandedTime, playerTime, playing]);
  const addCutSegment = useCallback(() => {
    const cutStartTime = currentCutSeg.start;
    const cutEndTime = currentCutSeg.end;
    if (cutStartTime === undefined && cutEndTime === undefined) return;
    const suggestedStart = getCurrentTime();
    const suggestedEnd = suggestedStart + 10;
    const cutSegmentsNew = [
      ...cutSegments,
      createSegment({
        start: suggestedStart,
        end: suggestedEnd <= duration ? suggestedEnd : undefined,
      }),
    ];
    setCutSegments(cutSegmentsNew);
    setCurrentSegIndex(cutSegmentsNew.length - 1);
  }, [
    currentCutSeg, cutSegments, getCurrentTime, duration, setCutSegments,
  ]);
  const setCutStart = useCallback(() => {
    // https://github.com/mifi/lossless-cut/issues/168
    // If we are after the end of the last segment in the timeline,
    // add a new segment that starts at playerTime
    if (currentCutSeg.end != null
      && getCurrentTime() > currentCutSeg.end) {
      addCutSegment();
    } else {
      try {
        setCutTime('start', getCurrentTime());
      } catch (err) {
        errorToast(err.message);
      }
    }
  }, [setCutTime, getCurrentTime, currentCutSeg, addCutSegment]);
  const setCutEnd = useCallback(() => {
    try {
      setCutTime('end', getCurrentTime());
    } catch (err) {
      errorToast(err.message);
    }
  }, [setCutTime, getCurrentTime]);
  async function setOutputDir() {
    const { filePaths } = await dialog.showOpenDialog({ properties: ['openDirectory'] });
    setCustomOutDir((filePaths && filePaths.length === 1) ? filePaths[0] : undefined);
  }
  const fileUri = (dummyVideoPath || html5FriendlyPath || filePath || '').replace(/#/g, '%23');
  function getOutputDir() {
    if (customOutDir) return customOutDir;
    if (filePath) return dirname(filePath);
    return undefined;
  }
  const outputDir = getOutputDir();
  // 360 means we don't modify rotation
  const isRotationSet = rotation !== 360;
  const effectiveRotation = isRotationSet ? rotation : undefined;
  const rotationStr = `${rotation}°`;
  useEffect(() => {
    async function throttledRender() {
      if (queue.size < 2) {
        queue.add(async () => {
          if (!frameRenderEnabled) return;
          if (playerTime == null || !filePath) return;
          try {
            const framePathNew = await ffmpeg.renderFrame(playerTime, filePath, effectiveRotation);
            setFramePath(framePathNew);
          } catch (err) {
            console.error(err);
          }
        });
      }
      await queue.onIdle();
    }
    throttledRender();
  }, [
    filePath, playerTime, frameRenderEnabled, effectiveRotation,
  ]);
  // Cleanup old frames
  useEffect(() => () => URL.revokeObjectURL(framePath), [framePath]);
  function onPlayingChange(val) {
    setPlaying(val);
    if (!val) {
      videoRef.current.playbackRate = 1;
      setCommandedTime(videoRef.current.currentTime);
    }
  }
  function onTimeUpdate(e) {
    const { currentTime } = e.target;
    if (playerTime === currentTime) return;
    setRotationPreviewRequested(false); // Reset this
    setPlayerTime(currentTime);
  }
  function increaseRotation() {
    setRotation((r) => (r + 90) % 450);
    setRotationPreviewRequested(true);
  }
  const offsetCurrentTime = (getCurrentTime() || 0) + startTimeOffset;
  const mergeFiles = useCallback(async ({ paths, allStreams }) => {
    try {
      setWorking(true);
      // console.log('merge', paths);
      await ffmpeg.mergeAnyFiles({
        customOutDir, paths, allStreams,
      });
    } catch (err) {
      errorToast('Failed to merge files. Make sure they are all of the exact same format and codecs');
      console.error('Failed to merge files', err);
    } finally {
      setWorking(false);
    }
  }, [customOutDir]);
  const toggleCaptureFormat = () => setCaptureFormat(f => (f === 'png' ? 'jpeg' : 'png'));
  const toggleKeyframeCut = () => setKeyframeCut(val => !val);
  const toggleAutoMerge = () => setAutoMerge(val => !val);
  const isCopyingStreamId = useCallback((path, streamId) => (
    !!(copyStreamIdsByFile[path] || {})[streamId]
  ), [copyStreamIdsByFile]);
  const copyAnyAudioTrack = mainStreams.some(stream => isCopyingStreamId(filePath, stream.index) && stream.codec_type === 'audio');
  // Streams that are not copy enabled by default
  const extraStreams = mainStreams
    .filter((stream) => !defaultProcessedCodecTypes.includes(stream.codec_type));
  // Extra streams that the user has not selected for copy
  const nonCopiedExtraStreams = extraStreams
    .filter((stream) => !isCopyingStreamId(filePath, stream.index));
  const exportExtraStreams = autoExportExtraStreams && nonCopiedExtraStreams.length > 0;
  const copyStreamIds = Object.entries(copyStreamIdsByFile).map(([path, streamIdsMap]) => ({
    path,
    streamIds: Object.keys(streamIdsMap).filter(index => streamIdsMap[index]),
  }));
  const numStreamsToCopy = copyStreamIds
    .reduce((acc, { streamIds }) => acc + streamIds.length, 0);
  const numStreamsTotal = [
    ...mainStreams,
    ...flatMap(Object.values(externalStreamFiles), ({ streams }) => streams),
  ].length;
  function toggleStripAudio() {
    setCopyStreamIdsForPath(filePath, (old) => {
      const newCopyStreamIds = { ...old };
      mainStreams.forEach((stream) => {
        if (stream.codec_type === 'audio') newCopyStreamIds[stream.index] = !copyAnyAudioTrack;
      });
      return newCopyStreamIds;
    });
  }
  const removeCutSegment = useCallback(() => {
    if (cutSegments.length < 2) return;
    const cutSegmentsNew = [...cutSegments];
    cutSegmentsNew.splice(currentSegIndexSafe, 1);
    setCutSegments(cutSegmentsNew);
  }, [currentSegIndexSafe, cutSegments, setCutSegments]);
  const jumpCutStart = () => seekAbs(currentApparentCutSeg.start);
  const jumpCutEnd = () => seekAbs(currentApparentCutSeg.end);
  function handleTap(e) {
    const target = timelineWrapperRef.current;
    const rect = target.getBoundingClientRect();
    const relX = e.srcEvent.pageX - (rect.left + document.body.scrollLeft);
    if (duration) seekAbs((relX / target.offsetWidth) * duration);
  }
  const durationSafe = duration || 1;
  const currentTimeWidth = 1;
  // Prevent it from overflowing (and causing scroll) when end of timeline
  const calculateTimelinePos = (time) => (time !== undefined && time < durationSafe ? `${(time / durationSafe) * 100}%` : undefined);
  const currentTimePos = calculateTimelinePos(playerTime);
  const commandedTimePos = calculateTimelinePos(commandedTime);
  const zoomed = zoom > 1;
  useEffect(() => {
    const { currentTime } = videoRef.current;
    timelineScrollerSkipEventRef.current = true;
    if (zoom > 1) {
      timelineScrollerRef.current.scrollLeft = (currentTime / durationSafe)
        * (timelineWrapperRef.current.offsetWidth - timelineScrollerRef.current.offsetWidth);
    }
  }, [zoom, durationSafe]);
  function onTimelineScroll(e) {
    if (timelineScrollerSkipEventRef.current) {
      timelineScrollerSkipEventRef.current = false;
      return;
    }
    if (!zoomed) return;
    seekAbs((((e.target.scrollLeft + (timelineScrollerRef.current.offsetWidth / 2))
      / timelineWrapperRef.current.offsetWidth) * duration));
  }
  function onWheel(e) {
    if (!zoomed) seekRel((e.deltaX + e.deltaY) / 15);
  }
  function showUnsupportedFileMessage() {
    toast.fire({ timer: 10000, icon: 'warning', title: 'This video is not natively supported', text: 'This means that there is no audio in the preview and it has low quality. The final export operation will however be lossless and contains audio!' });
  }
  const createDummyVideo = useCallback(async (fp) => {
    const html5ifiedDummyPathDummy = getOutPath(customOutDir, fp, 'html5ified-dummy.mkv');
    await ffmpeg.html5ifyDummy(fp, html5ifiedDummyPathDummy);
    setDummyVideoPath(html5ifiedDummyPathDummy);
    setHtml5FriendlyPath();
    showUnsupportedFileMessage();
  }, [customOutDir]);
  const tryCreateDummyVideo = useCallback(async () => {
    try {
      if (working) return;
      setWorking(true);
      await createDummyVideo(filePath);
    } catch (err) {
      console.error(err);
      errorToast('Failed to playback this file. Try to convert to friendly format from the menu');
    } finally {
      setWorking(false);
    }
  }, [createDummyVideo, filePath, working]);
  const playCommand = useCallback(() => {
    const video = videoRef.current;
    if (playing) return video.pause();
    return video.play().catch((err) => {
      console.error(err);
      if (err.name === 'NotSupportedError') {
        console.log('NotSupportedError, trying to create dummy');
        tryCreateDummyVideo(filePath);
      }
    });
  }, [playing, filePath, tryCreateDummyVideo]);
  const deleteSource = useCallback(async () => {
    if (!filePath) return;
    // eslint-disable-next-line no-alert
    if (working || !window.confirm(`Are you sure you want to move the source file to trash? ${filePath}`)) return;
    try {
      setWorking(true);
      await trash(filePath);
      if (html5FriendlyPath) await trash(html5FriendlyPath);
    } catch (err) {
      toast.fire({ icon: 'error', title: `Failed to trash source file: ${err.message}` });
    } finally {
      resetState();
    }
  }, [filePath, html5FriendlyPath, resetState, working]);
  const cutClick = useCallback(async () => {
    if (working) {
      errorToast('I\'m busy');
      return;
    }
    if (haveInvalidSegs) {
      errorToast('Start time must be before end time');
      return;
    }
    if (numStreamsToCopy === 0) {
      errorToast('No tracks to export!');
      return;
    }
    const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
    if (!segments) {
      errorToast('No segments to export!');
      return;
    }
    const ffmpegSegments = segments.map((seg) => ({
      cutFrom: seg.start,
      cutTo: seg.end,
    }));
    if (segments.length < 1) {
      errorToast('No segments to export');
      return;
    }
    try {
      setWorking(true);
      const outFiles = await ffmpeg.cutMultiple({
        customOutDir,
        filePath,
        outFormat: fileFormat,
        isOutFormatUserSelected: fileFormat !== detectedFileFormat,
        videoDuration: duration,
        rotation: effectiveRotation,
        copyStreamIds,
        keyframeCut,
        segments: ffmpegSegments,
        onProgress: setCutProgress,
        appendFfmpegCommandLog,
      });
      if (outFiles.length > 1 && autoMerge) {
        setCutProgress(0); // TODO implement progress
        await ffmpeg.autoMergeSegments({
          customOutDir,
          sourceFile: filePath,
          segmentPaths: outFiles,
        });
      }
      if (exportExtraStreams) {
        try {
          await ffmpeg.extractStreams({
            filePath, customOutDir, streams: nonCopiedExtraStreams,
          });
        } catch (err) {
          console.error('Extra stream export failed', err);
        }
      }
      toast.fire({ timer: 5000, icon: 'success', title: `Export completed! Output file(s) can be found at: ${getOutDir(customOutDir, filePath)}.${exportExtraStreams ? ' Extra unprocessable stream(s) exported as separate files.' : ''}` });
    } catch (err) {
      console.error('stdout:', err.stdout);
      console.error('stderr:', err.stderr);
      if (err.code === 1 || err.code === 'ENOENT') {
        toast.fire({ icon: 'error', title: `Whoops! ffmpeg was unable to export this video. Try one of the following before exporting again:\n1. Select a different output format from the ${fileFormat} button (matroska takes almost everything).\n2. Exclude unnecessary tracks\n3. Try "Normal cut" and "Keyframe cut"`, timer: 10000 });
        return;
      }
      showFfmpegFail(err);
    } finally {
      setWorking(false);
    }
  }, [
    effectiveRotation, apparentCutSegments, invertCutSegments, inverseCutSegments,
    working, duration, filePath, keyframeCut, detectedFileFormat,
    autoMerge, customOutDir, fileFormat, haveInvalidSegs, copyStreamIds, numStreamsToCopy,
    exportExtraStreams, nonCopiedExtraStreams,
  ]);
  // TODO use ffmpeg to capture frame
  const capture = useCallback(async () => {
    if (!filePath) return;
    if (html5FriendlyPath || dummyVideoPath) {
      errorToast('Capture frame from this video not yet implemented');
      return;
    }
    try {
      await captureFrame(customOutDir, filePath, videoRef.current, playerTime, captureFormat);
    } catch (err) {
      console.error(err);
      errorToast('Failed to capture frame');
    }
  }, [filePath, playerTime, captureFormat, customOutDir, html5FriendlyPath, dummyVideoPath]);
  const changePlaybackRate = useCallback((dir) => {
    const video = videoRef.current;
    if (!playing) {
      video.playbackRate = 0.5; // dir * 0.5;
      video.play();
    } else {
      const newRate = video.playbackRate + (dir * 0.15);
      video.playbackRate = clamp(newRate, 0.05, 16);
    }
  }, [playing]);
  const getHtml5ifiedPath = useCallback((fp, type) => getOutPath(customOutDir, fp, `html5ified-${type}.mp4`), [customOutDir]);
  const checkExistingHtml5FriendlyFile = useCallback(async (fp, speed) => {
    const existing = getHtml5ifiedPath(fp, speed);
    const ret = existing && await exists(existing);
    if (ret) {
      setHtml5FriendlyPath(existing);
      showUnsupportedFileMessage();
    }
    return ret;
  }, [getHtml5ifiedPath]);
  const load = useCallback(async (fp, html5FriendlyPathRequested) => {
    console.log('Load', { fp, html5FriendlyPathRequested });
    if (working) {
      errorToast('Tried to load file while busy');
      return;
    }
    resetState();
    setWorking(true);
    try {
      const ff = await ffmpeg.getFormat(fp);
      if (!ff) {
        errorToast('Unsupported file');
        return;
      }
      const { streams } = await ffmpeg.getAllStreams(fp);
      // console.log('streams', streamsNew);
      setStreams(streams);
      setCopyStreamIdsForPath(fp, () => fromPairs(streams.map((stream) => [
        stream.index, defaultProcessedCodecTypes.includes(stream.codec_type),
      ])));
      streams.find((stream) => {
        const streamFps = getStreamFps(stream);
        if (streamFps != null) {
          setDetectedFps(streamFps);
          return true;
        }
        return false;
      });
      setFileNameTitle(fp);
      setFilePath(fp);
      setFileFormat(ff);
      setDetectedFileFormat(ff);
      if (html5FriendlyPathRequested) {
        setHtml5FriendlyPath(html5FriendlyPathRequested);
        showUnsupportedFileMessage();
      } else if (
        !(await checkExistingHtml5FriendlyFile(fp, 'slow-audio') || await checkExistingHtml5FriendlyFile(fp, 'slow') || await checkExistingHtml5FriendlyFile(fp, 'fast'))
        && !doesPlayerSupportFile(streams)
      ) {
        await createDummyVideo(fp);
      }
    } catch (err) {
      if (err.code === 1 || err.code === 'ENOENT') {
        errorToast('Unsupported file');
        return;
      }
      showFfmpegFail(err);
    } finally {
      setWorking(false);
    }
  }, [resetState, working, createDummyVideo, checkExistingHtml5FriendlyFile]);
  const toggleHelp = () => setHelpVisible(val => !val);
  useEffect(() => {
    Mousetrap.bind('space', () => playCommand());
    Mousetrap.bind('k', () => playCommand());
    Mousetrap.bind('j', () => changePlaybackRate(-1));
    Mousetrap.bind('l', () => changePlaybackRate(1));
    Mousetrap.bind('left', () => seekRel(-1));
    Mousetrap.bind('right', () => seekRel(1));
    Mousetrap.bind('.', () => shortStep(1));
    Mousetrap.bind(',', () => shortStep(-1));
    Mousetrap.bind('c', () => capture());
    Mousetrap.bind('e', () => cutClick());
    Mousetrap.bind('i', () => setCutStart());
    Mousetrap.bind('o', () => setCutEnd());
    Mousetrap.bind('h', () => toggleHelp());
    Mousetrap.bind('+', () => addCutSegment());
    Mousetrap.bind('backspace', () => removeCutSegment());
    Mousetrap.bind('d', () => deleteSource());
    return () => {
      Mousetrap.unbind('space');
      Mousetrap.unbind('k');
      Mousetrap.unbind('j');
      Mousetrap.unbind('l');
      Mousetrap.unbind('left');
      Mousetrap.unbind('right');
      Mousetrap.unbind('.');
      Mousetrap.unbind(',');
      Mousetrap.unbind('c');
      Mousetrap.unbind('e');
      Mousetrap.unbind('i');
      Mousetrap.unbind('o');
      Mousetrap.unbind('h');
      Mousetrap.unbind('+');
      Mousetrap.unbind('backspace');
      Mousetrap.unbind('d');
    };
  }, [
    addCutSegment, capture, changePlaybackRate, cutClick, playCommand, removeCutSegment,
    setCutEnd, setCutStart, seekRel, shortStep, deleteSource,
  ]);
  useEffect(() => {
    document.ondragover = dragPreventer;
    document.ondragend = dragPreventer;
    electron.ipcRenderer.send('renderer-ready');
  }, []);
  useEffect(() => {
    electron.ipcRenderer.send('setAskBeforeClose', askBeforeClose);
  }, [askBeforeClose]);
  const extractAllStreams = useCallback(async () => {
    if (!filePath) return;
    try {
      setWorking(true);
      await ffmpeg.extractStreams({ customOutDir, filePath, streams: mainStreams });
      toast.fire({ icon: 'success', title: `All streams can be found as separate files at: ${getOutDir(customOutDir, filePath)}` });
    } catch (err) {
      errorToast('Failed to extract all streams');
      console.error('Failed to extract all streams', err);
    } finally {
      setWorking(false);
    }
  }, [customOutDir, filePath, mainStreams]);
  function onExtractAllStreamsPress() {
    extractAllStreams();
  }
  const addStreamSourceFile = useCallback(async (path) => {
    if (externalStreamFiles[path]) return;
    const { streams } = await ffmpeg.getAllStreams(path);
    // console.log('streams', streams);
    setExternalStreamFiles(old => ({ ...old, [path]: { streams } }));
    setCopyStreamIdsForPath(path, () => fromPairs(streams.map(({ index }) => [index, true])));
  }, [externalStreamFiles]);
  const userOpenFiles = useCallback(async (filePaths) => {
    if (filePaths.length < 1) return;
    if (filePaths.length > 1) {
      showMergeDialog(filePaths, mergeFiles);
      return;
    }
    const firstFile = filePaths[0];
    if (!filePath) {
      load(firstFile);
      return;
    }
    const { value } = await Swal.fire({
      title: 'You opened a new file. What do you want to do?',
      icon: 'question',
      input: 'radio',
      showCancelButton: true,
      inputOptions: {
        open: 'Open the file instead of the current one. You will lose all work',
        add: 'Include all tracks from the new file',
      },
      inputValidator: (v) => !v && 'You need to choose something!',
    });
    if (value === 'open') {
      load(firstFile);
    } else if (value === 'add') {
      addStreamSourceFile(firstFile);
      setStreamsSelectorShown(true);
    }
  }, [addStreamSourceFile, filePath, load, mergeFiles]);
  const onDrop = useCallback(async (ev) => {
    ev.preventDefault();
    const { files } = ev.dataTransfer;
    userOpenFiles(Array.from(files).map(f => f.path));
  }, [userOpenFiles]);
  useEffect(() => {
    function fileOpened(event, filePaths) {
      userOpenFiles(filePaths);
    }
    function closeFile() {
      // eslint-disable-next-line no-alert
      if (!window.confirm('Are you sure you want to close the current file? You will lose all work')) return;
      resetState();
    }
    async function html5ify(event, speed) {
      if (!filePath) return;
      try {
        setWorking(true);
        if (['fast', 'slow', 'slow-audio'].includes(speed)) {
          const html5FriendlyPathNew = getHtml5ifiedPath(filePath, speed);
          const encodeVideo = ['slow', 'slow-audio'].includes(speed);
          const encodeAudio = speed === 'slow-audio';
          await ffmpeg.html5ify(filePath, html5FriendlyPathNew, encodeVideo, encodeAudio);
          load(filePath, html5FriendlyPathNew);
        } else {
          await createDummyVideo(filePath);
        }
      } catch (err) {
        errorToast('Failed to html5ify file');
        console.error('Failed to html5ify file', err);
      } finally {
        setWorking(false);
      }
    }
    function showOpenAndMergeDialog2() {
      showOpenAndMergeDialog({
        dialog,
        defaultPath: outputDir,
        onMergeClick: mergeFiles,
      });
    }
    async function setStartOffset() {
      const newStartTimeOffset = await promptTimeOffset(
        startTimeOffset !== undefined ? formatDuration({ seconds: startTimeOffset }) : undefined,
      );
      if (newStartTimeOffset === undefined) return;
      setStartTimeOffset(newStartTimeOffset);
    }
    function undo() {
      cutSegmentsHistory.back();
    }
    function redo() {
      cutSegmentsHistory.forward();
    }
    electron.ipcRenderer.on('file-opened', fileOpened);
    electron.ipcRenderer.on('close-file', closeFile);
    electron.ipcRenderer.on('html5ify', html5ify);
    electron.ipcRenderer.on('show-merge-dialog', showOpenAndMergeDialog2);
    electron.ipcRenderer.on('set-start-offset', setStartOffset);
    electron.ipcRenderer.on('extract-all-streams', extractAllStreams);
    electron.ipcRenderer.on('undo', undo);
    electron.ipcRenderer.on('redo', redo);
    return () => {
      electron.ipcRenderer.removeListener('file-opened', fileOpened);
      electron.ipcRenderer.removeListener('close-file', closeFile);
      electron.ipcRenderer.removeListener('html5ify', html5ify);
      electron.ipcRenderer.removeListener('show-merge-dialog', showOpenAndMergeDialog2);
      electron.ipcRenderer.removeListener('set-start-offset', setStartOffset);
      electron.ipcRenderer.removeListener('extract-all-streams', extractAllStreams);
      electron.ipcRenderer.removeListener('undo', undo);
      electron.ipcRenderer.removeListener('redo', redo);
    };
  }, [
    load, mergeFiles, outputDir, filePath, customOutDir, startTimeOffset, getHtml5ifiedPath,
    createDummyVideo, resetState, extractAllStreams, userOpenFiles, cutSegmentsHistory,
  ]);
  async function showAddStreamSourceDialog() {
    const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openFile'] });
    if (canceled || filePaths.length < 1) return;
    await addStreamSourceFile(filePaths[0]);
  }
  useEffect(() => {
    document.body.addEventListener('drop', onDrop);
    return () => document.body.removeEventListener('drop', onDrop);
  }, [load, mergeFiles, onDrop]);
  function renderCutTimeInput(type) {
    const cutTimeManual = type === 'start' ? cutStartTimeManual : cutEndTimeManual;
    const cutTimeInputStyle = { width: '8em', textAlign: type === 'start' ? 'right' : 'left' };
    const isCutTimeManualSet = () => cutTimeManual !== undefined;
    const set = type === 'start' ? setCutStartTimeManual : setCutEndTimeManual;
    const handleCutTimeInput = (text) => {
      // Allow the user to erase
      if (text.length === 0) {
        set();
        return;
      }
      const time = parseDuration(text);
      if (time === undefined) {
        set(text);
        return;
      }
      set();
      const rel = time - startTimeOffset;
      try {
        setCutTime(type, rel);
      } catch (err) {
        console.error('Cannot set cut time', err);
      }
      seekAbs(rel);
    };
    const cutTime = type === 'start' ? currentApparentCutSeg.start : currentApparentCutSeg.end;
    return (
       handleCutTimeInput(e.target.value)}
        value={isCutTimeManualSet()
          ? cutTimeManual
          : formatDuration({ seconds: cutTime + startTimeOffset })}
      />
    );
  }
  const commonFormats = ['mov', 'mp4', 'matroska', 'mp3', 'ipod'];
  const commonFormatsMap = fromPairs(commonFormats.map(format => [format, allOutFormats[format]])
    .filter(([f]) => f !== detectedFileFormat));
  const otherFormatsMap = fromPairs(Object.entries(allOutFormats)
    .filter(([f]) => ![...commonFormats, detectedFileFormat].includes(f)));
  const segColor = (currentCutSeg || {}).color;
  const segBgColor = segColor.alpha(0.5).string();
  const segActiveBgColor = segColor.lighten(0.5).alpha(0.5).string();
  const segBorderColor = segColor.lighten(0.5).string();
  const jumpCutButtonStyle = {
    position: 'absolute', color: 'black', bottom: 0, top: 0, padding: '2px 8px',
  };
  function renderFormatOptions(map) {
    return Object.entries(map).map(([f, name]) => (
      
    ));
  }
  function renderOutFmt({ width } = {}) {
    return (
      
    );
  }
  function renderCaptureFormatButton() {
    return (
      
    );
  }
  const renderSettings = () => (
    
         
      Output format (default autodetected) 
        {renderOutFmt()} 
      
         
      Output directory 
        
          
           
      
         
      Auto merge segments to one file after export? 
        
          
         
      
         
      keyframe cut mode 
        
          
         
      
         
      
          Discard (cut away) or keep selected segments from video when exporting
         
        
          
         
      
         
      
          Discard audio?
         
        
          
         
      
         
      
          Extract unprocessable tracks to separate files? 
        
          (data tracks such as GoPro GPS, telemetry etc. are not copied over by default because ffmpeg cannot cut them, thus they will cause the media duration to stay the same after cutting video/audio)
        
          
         
      
         
      
          Snapshot capture format
         
        
          {renderCaptureFormatButton()}
         
      
         
      In timecode show 
        
          
         
      
         
    Ask for confirmation when closing app?