import { DateRange, PlayArrow, Stop } from "@mui/icons-material";
import { Box, Button, DialogActions, DialogContent, FormControl, IconButton, LinearProgress, MenuItem, Select, TextField, Typography } from "@mui/material";
import { grey } from "@mui/material/colors";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useMobile } from "hooks";
import { cloneDeep } from "lodash";
import { Bin } from "models";
import moment, { Moment } from "moment";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useBinAPI, useComponentAPI } from "providers";
import React, { useEffect, useRef, useState } from "react";

const CHECKPOINT_COUNT = 10;

interface StatusCheckpoint {
  time: Moment;
  timeString: string;
  statusBushels: number;
  cables: pond.GrainCable[];
  fans: pond.BinFan[];
  heaters: pond.BinHeater[];
}

interface Props {
  bin: Bin;
  setBin: React.Dispatch<React.SetStateAction<Bin>>;
  componentDevices: Map<string, number>;
}

type ReadingSlot = {
  timestamp: string;
  values: number[];
};

type CableReadingsAtCheckpoint = {
  temperature?: ReadingSlot;
  humidity?: ReadingSlot;
  moisture?: ReadingSlot;
};

/**
 * Per-checkpoint bucket for boolean output components (fans, heaters).
 * Only the most recent boolean reading in the time slice is kept.
 */
type BooleanStateAtCheckpoint = {
  state?: ReadingSlot; // values[0] is the boolean (1 = on, 0 = off)
};

/** Per-checkpoint bucket: cable key → latest temp/humidity/moisture readings in that time slice. */
type CheckpointReadings = Map<string, CableReadingsAtCheckpoint>;

/** Per-checkpoint bucket: component key → latest boolean state in that time slice. */
type CheckpointBooleanStates = Map<string, BooleanStateAtCheckpoint>;

/**
 * Returns true when `candidate` should replace `existing` in a bucket.
 * Used so multiple samples in the same checkpoint keep only the newest reading per type.
 */
function readingIsNewer(candidate: ReadingSlot, existing?: ReadingSlot): boolean {
  if (!existing) {
    return true;
  }
  return moment(candidate.timestamp).valueOf() > moment(existing.timestamp).valueOf();
}

/**
 * Stores one sampled reading on a cable's in-progress bucket state.
 * Routes by measurement type: temperature → celcius nodes, percent RH → humidity nodes,
 * grain EMC → moisture nodes. Ignores the update if an equal-or-newer reading is already stored.
 */
function upsertReading(
  readings: CableReadingsAtCheckpoint,
  type: quack.MeasurementType,
  timestamp: string,
  values: number[]
): void {
  const slot: ReadingSlot = { timestamp, values };
  if (type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
    if (readingIsNewer(slot, readings.temperature)) {
      readings.temperature = slot;
    }
  } else if (type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
    if (readingIsNewer(slot, readings.humidity)) {
      readings.humidity = slot;
    }
  } else if (type === quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC) {
    if (readingIsNewer(slot, readings.moisture)) {
      readings.moisture = slot;
    }
  }
}

/**
 * A trimmed record of a single component-settings history entry: just the timestamp and the
 * top-node value (`grain_filled_to`) that was in effect from that point forward.
 */
type TopNodeHistoryEntry = {
  timestamp: Moment;
  topNode: number;
};

function buildTopNodeHistory(
  history: pond.ComponentHistory[]
): TopNodeHistoryEntry[] {
  return history
    .filter(h => h.component && h.component.grainFilledTo > 0 && h.timestamp)
    .map(h => ({
      timestamp: moment(h.timestamp),
      topNode: h.component ? h.component.grainFilledTo : 0,
    }))
    .sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());
}

function topNodeAtTime(
  history: TopNodeHistoryEntry[],
  checkpointTime: Moment
): number | undefined {
  if (history.length === 0) return undefined;
  const t = checkpointTime.valueOf();
  let lo = 0;
  let hi = history.length - 1;
  let result: number | undefined = undefined;
  while (lo <= hi) {
    const mid = (lo + hi) >>> 1;
    if (history[mid].timestamp.valueOf() <= t) {
      result = history[mid].topNode;
      lo = mid + 1;
    } else {
      hi = mid - 1;
    }
  }
  return result;
}

/**
 * A single recorded bushel estimate with its timestamp, sourced from bin object measurements.
 */
type BushelEntry = {
  timestamp: Moment;
  bushels: number;
};

function buildBushelHistory(
  response: pond.ListObjectMeasurementsResponse
): BushelEntry[] {
  const entries: BushelEntry[] = [];

  response.measurements.forEach(um => {
    const isBushelType =
      um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_LIDAR ||
      um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_CABLE ||
      um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_LIBRACART;

    if (!isBushelType) return;

    um.timestamps.forEach((ts, i) => {
      const valueArray = um.values[i];
      if (!ts || !valueArray || valueArray.error || valueArray.values.length === 0) return;
      entries.push({
        timestamp: moment(ts),
        bushels: valueArray.values[0],
      });
    });
  });

  return entries.sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());
}

function bushelsAtTime(
  history: BushelEntry[],
  checkpointTime: Moment
): number | undefined {
  if (history.length === 0) return undefined;
  const t = checkpointTime.valueOf();
  let lo = 0;
  let hi = history.length - 1;
  let result: number | undefined = undefined;
  while (lo <= hi) {
    const mid = (lo + hi) >>> 1;
    if (history[mid].timestamp.valueOf() <= t) {
      result = history[mid].bushels;
      lo = mid + 1;
    } else {
      hi = mid - 1;
    }
  }
  return result;
}

/**
 * Produces `count` wall-clock times evenly spaced from `start` through `end` (inclusive).
 * These are the playback frames — not derived from measurement data.
 */
function buildCheckpointTimes(start: Moment, end: Moment, count: number): Moment[] {
  const startMs = start.valueOf();
  const endMs = end.valueOf();
  if (count <= 1 || endMs <= startMs) {
    return [moment(startMs)];
  }
  const span = endMs - startMs;
  return Array.from({ length: count }, (_, i) =>
    moment(startMs + (span * i) / (count - 1))
  );
}

/**
 * Maps a measurement timestamp to a checkpoint bucket index in [0, count - 1].
 * The [start, end] range is split into `count` equal-width segments; a reading lands in
 * the segment it falls into. Times before start → 0, after end → last bucket.
 */
function checkpointIndexForTimestamp(
  timestamp: Moment,
  start: Moment,
  end: Moment,
  count: number
): number {
  const startMs = start.valueOf();
  const endMs = end.valueOf();
  if (count <= 1 || endMs <= startMs) {
    return 0;
  }
  const t = timestamp.valueOf();
  if (t <= startMs) {
    return 0;
  }
  if (t >= endMs) {
    return count - 1;
  }
  const fraction = (t - startMs) / (endMs - startMs);
  const idx = Math.floor(fraction * count);
  return Math.min(idx, count - 1);
}

/**
 * Creates `count` empty buckets, each pre-populated with an empty reading map per cable key.
 * Output length matches checkpoint count; index aligns with `buildCheckpointTimes`.
 */
function emptyCheckpointReadings(cableKeys: string[], count: number): CheckpointReadings[] {
  return Array.from({ length: count }, () => {
    const map: CheckpointReadings = new Map();
    cableKeys.forEach(key => map.set(key, {}));
    return map;
  });
}

/**
 * Creates `count` empty boolean state buckets, each pre-populated per component key.
 */
function emptyCheckpointBooleanStates(
  componentKeys: string[],
  count: number
): CheckpointBooleanStates[] {
  return Array.from({ length: count }, () => {
    const map: CheckpointBooleanStates = new Map();
    componentKeys.forEach(key => map.set(key, {}));
    return map;
  });
}

/**
 * Walks every sampled measurement from each cable response and places it into a time bucket.
 * `measurementResponses[i]` corresponds to `cableKeys[i]` (same order as the sample requests).
 * Within a bucket, only the newest reading per measurement type is kept per cable.
 */
function assignMeasurementsToCheckpoints(
  cableKeys: string[],
  measurementResponses: pond.SampleUnitMeasurementsResponse[],
  start: Moment,
  end: Moment,
  count: number
): CheckpointReadings[] {
  const buckets = emptyCheckpointReadings(cableKeys, count);

  measurementResponses.forEach((response, cableIndex) => {
    const cableKey = cableKeys[cableIndex];
    if (!cableKey) {
      return;
    }

    response.measurements.forEach(um => {
      um.timestamps.forEach((ts, i) => {
        const valueArray = um.values[i];
        if (!ts || !valueArray || valueArray.error) {
          return;
        }
        const idx = checkpointIndexForTimestamp(moment(ts), start, end, count);
        const readings = buckets[idx].get(cableKey)!;
        upsertReading(readings, um.type, ts, valueArray.values);
      });
    });
  });

  return buckets;
}

/**
 * Walks every sampled measurement from each boolean output component (fan/heater) response
 * and places the boolean state into a time bucket. Only the newest MEASUREMENT_TYPE_BOOLEAN
 * reading per component per bucket is kept.
 */
function assignBooleanStatesToCheckpoints(
  componentKeys: string[],
  measurementResponses: pond.SampleUnitMeasurementsResponse[],
  start: Moment,
  end: Moment,
  count: number
): CheckpointBooleanStates[] {
  const buckets = emptyCheckpointBooleanStates(componentKeys, count);

  measurementResponses.forEach((response, componentIndex) => {
    const componentKey = componentKeys[componentIndex];
    if (!componentKey) {
      return;
    }

    response.measurements.forEach(um => {
      if (um.type !== quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN) {
        return;
      }
      um.timestamps.forEach((ts, i) => {
        const valueArray = um.values[i];
        if (!ts || !valueArray || valueArray.error || valueArray.values.length === 0) {
          return;
        }
        const slot: ReadingSlot = { timestamp: ts, values: valueArray.values };
        const idx = checkpointIndexForTimestamp(moment(ts), start, end, count);
        const entry = buckets[idx].get(componentKey)!;
        if (readingIsNewer(slot, entry.state)) {
          entry.state = slot;
        }
      });
    });
  });

  return buckets;
}

/**
 * Picks the newest timestamp across temp, humidity, and moisture slots for a cable.
 * Used as `lastRead` on the reconstructed `pond.GrainCable`.
 */
function latestTimestamp(readings: CableReadingsAtCheckpoint): string | undefined {
  const slots = [readings.temperature, readings.humidity, readings.moisture].filter(
    (s): s is ReadingSlot => s !== undefined
  );
  if (slots.length === 0) {
    return undefined;
  }
  return slots.reduce(
    (latest, slot) =>
      moment(slot.timestamp).valueOf() > moment(latest).valueOf() ? slot.timestamp : latest,
    slots[0].timestamp
  );
}

/**
 * Combines readings carried forward from earlier checkpoints with readings in the current bucket.
 * Any type missing in the current bucket keeps the previous checkpoint's value so playback
 * does not drop cables back to template defaults between sparse samples.
 */
function mergeCarriedReadings(
  carried: CableReadingsAtCheckpoint,
  current: CableReadingsAtCheckpoint
): CableReadingsAtCheckpoint {
  return {
    temperature: current.temperature ?? carried.temperature,
    humidity: current.humidity ?? carried.humidity,
    moisture: current.moisture ?? carried.moisture
  };
}

/**
 * Combines boolean states carried forward from earlier checkpoints with the current bucket.
 * If no reading exists in the current bucket the last known state is preserved, so fans/heaters
 * don't flicker back to their template default between sparse samples.
 */
function mergeCarriedBooleanState(
  carried: BooleanStateAtCheckpoint,
  current: BooleanStateAtCheckpoint
): BooleanStateAtCheckpoint {
  return {
    state: current.state ?? carried.state
  };
}

/**
 * Clones a live `pond.GrainCable` template (name, key, device, top node, etc.) and overwrites
 * node arrays from reconciled readings. `lastRead` is the newest measurement time, or the
 * checkpoint time when no readings exist yet for that cable.
 */
function buildGrainCableFromReadings(
  template: pond.GrainCable,
  readings: CableReadingsAtCheckpoint,
  checkpointTime: Moment
): pond.GrainCable {
  const cable = pond.GrainCable.fromObject(cloneDeep(template));

  // Clear template values so empty checkpoints don't inherit live data
  cable.celcius = [];
  cable.relativeHumidity = [];
  cable.moisture = [];

  if (readings.temperature) {
    cable.celcius = [...readings.temperature.values];
  }
  if (readings.humidity) {
    cable.relativeHumidity = [...readings.humidity.values];
  }
  if (readings.moisture) {
    cable.moisture = [...readings.moisture.values];
  }

  cable.lastRead = latestTimestamp(readings) ?? checkpointTime.toISOString();
  return cable;
}

/**
 * Clones a `pond.BinFan` template and overwrites its `state` from the reconciled boolean
 * reading. If no reading has been seen yet the template state is left as-is (carry-forward
 * will eventually set it once the first sample arrives).
 */
function buildBinFanFromState(
  template: pond.BinFan,
  booleanState: BooleanStateAtCheckpoint
): pond.BinFan {
  const fan = pond.BinFan.fromObject(cloneDeep(template));
  if (booleanState.state !== undefined) {
    fan.state = booleanState.state.values[0] !== 0;
  }
  return fan;
}

/**
 * Clones a `pond.BinHeater` template and overwrites its `state` from the reconciled boolean
 * reading. Same carry-forward semantics as `buildBinFanFromState`.
 */
function buildBinHeaterFromState(
  template: pond.BinHeater,
  booleanState: BooleanStateAtCheckpoint
): pond.BinHeater {
  const heater = pond.BinHeater.fromObject(cloneDeep(template));
  if (booleanState.state !== undefined) {
    heater.state = booleanState.state.values[0] !== 0;
  }
  return heater;
}

/**
 * End-to-end pipeline: evenly spaced times → bucket measurements → carry-forward merge → cables + fans + heaters.
 * Returns one `StatusCheckpoint` per frame with `time` (playback moment), `cables`
 * (full `grainCables` array), `fans`, and `heaters` as they would have appeared at that moment.
 */
function buildStatusCheckpoints(
  cableTemplates: pond.GrainCable[],
  fanTemplates: pond.BinFan[],
  heaterTemplates: pond.BinHeater[],
  cableMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
  fanMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
  heaterMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
  /** Sorted top-node history per cable key. Pass an empty Map when not needed. */
  topNodeHistories: Map<string, TopNodeHistoryEntry[]>,
  /** Sorted bushel history derived from bin object measurements. */
  bushelHistory: BushelEntry[],
  /** Fallback bushel count used when no measurement precedes a checkpoint. */
  fallbackBushels: number,
  start: Moment,
  end: Moment,
  count: number = CHECKPOINT_COUNT
): StatusCheckpoint[] {
  const times = buildCheckpointTimes(start, end, count);
  console.log(times);

  // --- cables ---
  const cableKeys = cableTemplates.map(c => c.key);
  const cableBuckets = assignMeasurementsToCheckpoints(
    cableKeys,
    cableMeasurementResponses,
    start,
    end,
    count
  );

  // --- fans ---
  const fanKeys = fanTemplates.map(f => f.key);
  const fanBuckets = assignBooleanStatesToCheckpoints(
    fanKeys,
    fanMeasurementResponses,
    start,
    end,
    count
  );

  // --- heaters ---
  const heaterKeys = heaterTemplates.map(h => h.key);
  const heaterBuckets = assignBooleanStatesToCheckpoints(
    heaterKeys,
    heaterMeasurementResponses,
    start,
    end,
    count
  );

  // Carry-forward state per component
  const carriedByCable = new Map<string, CableReadingsAtCheckpoint>();
  cableKeys.forEach(key => carriedByCable.set(key, {}));

  const carriedByFan = new Map<string, BooleanStateAtCheckpoint>();
  fanKeys.forEach(key => carriedByFan.set(key, {}));

  const carriedByHeater = new Map<string, BooleanStateAtCheckpoint>();
  heaterKeys.forEach(key => carriedByHeater.set(key, {}));

  return cableBuckets.map((cableBucket, i) => {
    const time = times[i];

    // Build cables
    const cables = cableTemplates.map(template => {
      const key = template.key;
      const bucketReadings = cableBucket.get(key) ?? {};
      const merged = mergeCarriedReadings(carriedByCable.get(key)!, bucketReadings);
      carriedByCable.set(key, merged);
      const cable = buildGrainCableFromReadings(template, merged, time);

      // Apply the most recent top-node setting that was in effect at this checkpoint time.
      // Falls back to the template's current top_node when no history entry precedes this time.
      const history = topNodeHistories.get(key);
      if (history) {
        const tn = topNodeAtTime(history, time);
        if (tn !== undefined) {
          cable.topNode = tn;
        }
      }

      return cable;
    });

    // Build fans
    const fans = fanTemplates.map(template => {
      const key = template.key;
      const bucketState = fanBuckets[i].get(key) ?? {};
      const merged = mergeCarriedBooleanState(carriedByFan.get(key)!, bucketState);
      carriedByFan.set(key, merged);
      return buildBinFanFromState(template, merged);
    });

    // Build heaters
    const heaters = heaterTemplates.map(template => {
      const key = template.key;
      const bucketState = heaterBuckets[i].get(key) ?? {};
      const merged = mergeCarriedBooleanState(carriedByHeater.get(key)!, bucketState);
      carriedByHeater.set(key, merged);
      return buildBinHeaterFromState(template, merged);
    });

    // Resolve bushels: binary search gives the most recent recorded value at or before this
    // checkpoint (carry-forward is implicit in the search). Falls back to the current live
    // status bushels when the playback range predates all recorded measurements.
    const statusBushels = bushelsAtTime(bushelHistory, time) ?? fallbackBushels;

    return { time, cables, fans, heaters, timeString: time.toISOString(), statusBushels };
  }).filter(checkpoint =>
    // Keep checkpoints that have at least some cable data
    checkpoint.cables.some(cable =>
      cable.celcius.length > 0 ||
      cable.relativeHumidity.length > 0 ||
      cable.moisture.length > 0
    )
  );
}

/**
 * Calculates the number of checkpoints to create based on the start and end dates.
 * Is hard coded to space the checkpoints 6 hours apart.
 * @param start - The start date
 * @param end - The end date
 * @returns The number of checkpoints to create
 */
function checkpointCountFromRange(start: Moment, end: Moment): number {
  const hours = end.diff(start, 'hours');
  return Math.max(1, Math.floor(hours / 6) + 1);
}

/**
 * This component is used to reconcile measurements with an array of bin objects in order to see how the bin has progressed over a set period.
 * It gets the measurements for connected components, and uses those measurements to create a bin and simulate what its status would have been
 * at that time, creates an array of bins doing this and then uses a timer in a loop to change the bin in the status.
 * Fans and heaters are included so playback also reflects which equipment was running at each checkpoint.
 * @returns a JSX Element
 */
export default function BinPlayer(props: Props) {
  const { bin, componentDevices, setBin } = props;
  const isMobile = useMobile();
  const componentAPI = useComponentAPI();
  const binAPI = useBinAPI();
  const [dateSelect, setDateSelect] = useState<number>(0);
  const [startDate, setStartDate] = useState<Moment>(moment().subtract(1, 'week'));
  const [endDate, setEndDate] = useState<Moment>(moment);
  const [dateRangeDialog, setDateRangeDialog] = useState(false);
  const [tempStartDate, setTempStartDate] = useState<Moment>(moment().subtract(1, 'week'));
  const [tempEndDate, setTempEndDate] = useState<Moment>(moment());
  const [currentCheckpointTime, setCurrentCheckpointTime] = useState<Moment | undefined>(undefined);
  const [displayProgress, setDisplayProgress] = useState(0);
  const totalCheckpoints = useRef(0);
  const checkpointStartTime = useRef(0);
  const currentCheckpointIndex = useRef(0);
  const isPlaying = useRef(false);
  const PLAYBACK_INTERVAL = 1000;
  const [isPlayingState, setIsPlayingState] = useState(false);
  const stopPlayer = () => { isPlaying.current = false; };
  const originalBin = useRef<Bin | null>(null);

  useEffect(() => {
    if (!isPlayingState) return;

    const fps = 30;
    const interval = 1000 / fps;
    const stepDuration = PLAYBACK_INTERVAL;

    const timer = setInterval(() => {
      if (!isPlayingState) {
        clearInterval(timer);
        setDisplayProgress(0);
        return;
      }
      const total = totalCheckpoints.current;
      if (total === 0) return;

      const idx = currentCheckpointIndex.current;
      const elapsed = Date.now() - checkpointStartTime.current;
      const stepProgress = Math.min(elapsed / stepDuration, 1);
      const raw = total <= 1 ? 100 : ((idx + stepProgress) / (total - 1)) * 100;
      setDisplayProgress(Math.min(raw, 100));
    }, interval);

    return () => clearInterval(timer);
  }, [isPlayingState]);

  /**
   * Fetches sampled unit measurements for every grain cable, fan, and heater in the current
   * bin status, then builds `statusCheckpoints` for playback over [startDate, endDate].
   */
  const startPlayer = async () => {
    originalBin.current = cloneDeep(bin);
    isPlaying.current = true;
    setIsPlayingState(true);

    // Cable requests
    const cableSampleRequests = bin.status.grainCables.map(cable => {
      const deviceID = componentDevices.get(cable.key) ?? 0;
      return componentAPI.sampleUnitMeasurements(deviceID, cable.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
    });

    // Cable top-node history requests — one per cable, covering the full playback range.
    const cableHistoryRequests = bin.status.grainCables.map(cable => {
      const deviceID = componentDevices.get(cable.key) ?? 0;
      return componentAPI.listHistoryBetween(
        deviceID,
        cable.key,
        startDate.toISOString(),
        endDate.toISOString()
      );
    });

    // Fan requests
    const fanSampleRequests = bin.status.fans.map(fan => {
      const deviceID = componentDevices.get(fan.key) ?? 0;
      return componentAPI.sampleUnitMeasurements(deviceID, fan.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
    });

    // Heater requests
    const heaterSampleRequests = bin.status.heaters.map(heater => {
      const deviceID = componentDevices.get(heater.key) ?? 0;
      return componentAPI.sampleUnitMeasurements(deviceID, heater.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
    });

    const binMeasurementsRequest = binAPI.listBinMeasurements(bin.key(), startDate, endDate, 0, 0, 'asc');

    const [cableResponses, fanResponses, heaterResponses, cableHistoryResponses, binMeasurementsResponse] = await Promise.all([
      Promise.all(cableSampleRequests),
      Promise.all(fanSampleRequests),
      Promise.all(heaterSampleRequests),
      Promise.all(cableHistoryRequests),
      binMeasurementsRequest,
    ]);

    // Build a per-cable sorted top-node history map for use during checkpoint construction.
    const topNodeHistories = new Map<string, TopNodeHistoryEntry[]>();
    bin.status.grainCables.forEach((cable, i) => {
      const historyResp = cableHistoryResponses[i];
      const entries = buildTopNodeHistory(historyResp?.data?.history ?? []);
      topNodeHistories.set(cable.key, entries);
    });

    // Build sorted bushel history from bin object measurements.
    const bushelHistory = buildBushelHistory(binMeasurementsResponse.data);

    const checkpoints = buildStatusCheckpoints(
      bin.status.grainCables,
      bin.status.fans,
      bin.status.heaters,
      cableResponses.map(r => r.data),
      fanResponses.map(r => r.data),
      heaterResponses.map(r => r.data),
      topNodeHistories,
      bushelHistory,
      bin.status.grainBushels,
      startDate,
      endDate,
      checkpointCountFromRange(startDate, endDate)
    );

    for (let i = 0; i < checkpoints.length; i++) {
      if (!isPlaying.current) {
        setBin(originalBin.current!);
        setCurrentCheckpointTime(undefined);
        setIsPlayingState(false);
        return;
      }
      currentCheckpointIndex.current = i;
      totalCheckpoints.current = checkpoints.length;
      checkpointStartTime.current = Date.now();

      const newBin = cloneDeep(originalBin.current!);
      newBin.status.grainCables = checkpoints[i].cables;
      newBin.status.fans = checkpoints[i].fans;
      newBin.status.heaters = checkpoints[i].heaters;
      newBin.status.grainBushels = checkpoints[i].statusBushels;

      setBin(newBin);
      setCurrentCheckpointTime(checkpoints[i].time);
      await new Promise(res => setTimeout(res, PLAYBACK_INTERVAL));
    }

    isPlaying.current = false;
    setIsPlayingState(false);
    setBin(originalBin.current!);
    setCurrentCheckpointTime(undefined);
  };

  const datePickerDialog = () => {
    return (
      <ResponsiveDialog
        open={dateRangeDialog}
        onClose={() => setDateRangeDialog(false)}
        aria-labelledby="date-range-dialog">
        <DialogContent>
          <TextField
            fullWidth
            margin="normal"
            type="date"
            label="Start Date"
            value={tempStartDate.format("YYYY-MM-DD")}
            onChange={e => {
              setTempStartDate(moment(e.target.value));
            }}
          />
          <TextField
            fullWidth
            margin="normal"
            type="date"
            label="End Date"
            value={tempEndDate.format("YYYY-MM-DD")}
            onChange={e => {
              setTempEndDate(moment(e.target.value));
            }}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setDateRangeDialog(false)} color="primary">
            Close
          </Button>
          <Button onClick={() => {
            const now = moment();
            setStartDate(tempStartDate.clone().set({
              hour: now.hour(),
              minute: now.minute(),
              second: now.second()
            }));
            setEndDate(tempEndDate.clone().set({
              hour: now.hour(),
              minute: now.minute(),
              second: now.second()
            }));
            setDateRangeDialog(false);
          }} color="primary">
            Submit
          </Button>
        </DialogActions>
      </ResponsiveDialog>
    );
  };

  const dateSelector = () => {
    return (
      <Box display="flex" flexDirection="row" justifyContent="space-between">
        <FormControl fullWidth>
          <Select
            value={dateSelect}
            sx={{
              fontSize: 12,
              borderRadius: 2,
              border: "1px solid",
              borderColor: grey[800],
              '& .MuiSelect-select': {
                py: 1.5,
              },
              '& .MuiOutlinedInput-notchedOutline': { border: 'none' },
              '&:hover .MuiOutlinedInput-notchedOutline': { border: 'none' },
              '&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' },
            }}
            onChange={event => {
              setDateSelect(event.target.value as number);
              let currentDate = moment();
              setEndDate(currentDate);
              switch (event.target.value) {
                case 0:
                  setStartDate(currentDate.clone().subtract(1, 'week'));
                  break;
                case 1:
                  setStartDate(currentDate.clone().subtract(2, 'weeks'));
                  break;
                case 2:
                  setStartDate(currentDate.clone().subtract(4, 'weeks'));
                  break;
              }
            }}
          >
            <MenuItem value={0}>1 Week</MenuItem>
            <MenuItem value={1}>2 Weeks</MenuItem>
            <MenuItem value={2}>4 Weeks</MenuItem>
            <MenuItem value={3}>Custom</MenuItem>
          </Select>
        </FormControl>
        {dateSelect === 3 && (
          <Button onClick={() => setDateRangeDialog(true)} color="primary"><DateRange /></Button>
        )}
      </Box>
    );
  };

  return (
    <React.Fragment>
      {datePickerDialog()}
      <Box display="flex" flexDirection="row" justifyContent="space-between" width="100%" gap={2} alignContent={"center"} alignItems={"center"}>
        {/* start/stop button */}
        <Box>
          {isPlayingState ? (
            <IconButton sx={{ border: "1px solid", borderRadius: "50%", padding: 1, height: isMobile ? 40 : 60, width: isMobile ? 40 : 60 }} onClick={stopPlayer}><Stop fontSize="large" /></IconButton>
          ) : (
            <IconButton sx={{ border: "1px solid", borderRadius: "50%", padding: 1, height: isMobile ? 40 : 60, width: isMobile ? 40 : 60 }} onClick={startPlayer}><PlayArrow fontSize="large" /></IconButton>
          )}
        </Box>
        {/* progress bar and date display */}
        <Box sx={{ flexGrow: 1 }}>
          <Typography fontSize={isMobile ? 15 : 17} textAlign={"center"}>{currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")}</Typography>
          <LinearProgress value={displayProgress} variant="determinate" color="inherit" />
          <Box display="flex" flexDirection="row" justifyContent="space-between">
            <Typography variant="caption">{startDate.format("MMM D, YYYY")}</Typography>
            <Typography variant="caption">{endDate.format("MMM D, YYYY")}</Typography>
          </Box>
        </Box>
        {/* date range selector */}
        <Box>
          {dateSelector()}
        </Box>
      </Box>
    </React.Fragment>
  );
}