package com.replaymod.simplepathing;

import com.google.common.collect.Iterables;
import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.replaystudio.pathing.change.Change;
import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator;
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.PathSegment;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.util.EntityPositionTracker;
import com.replaymod.replaystudio.util.Location;
import com.replaymod.simplepathing.SPTimeline.SPPath;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.logging.log4j.LogManager;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

public class SPTimelineTest {
    private SPTimeline impl;
    private Timeline timeline;

    @BeforeClass
    public static void setupLogger() {
        ReplayModSimplePathing.LOGGER = LogManager.getLogger();
    }

    @Before
    public void setup() {
        impl = new SPTimeline();
        impl.setDefaultInterpolatorType(InterpolatorType.CUBIC);
        impl.setEntityTracker(new EntityPositionTrackerMock());
        timeline = impl.getTimeline();
    }

    @Test
    public void testGetPath() {
        assertSame(impl.getTimeline().getPaths().get(0), impl.getTimePath());
        assertSame(impl.getTimeline().getPaths().get(0), impl.getPath(SPPath.TIME));
        assertSame(impl.getTimeline().getPaths().get(1), impl.getPositionPath());
        assertSame(impl.getTimeline().getPaths().get(1), impl.getPath(SPPath.POSITION));
    }

    @Test
    public void testGetKeyframe() {
        assertSame(timeline.getPaths().get(0).insert(123), impl.getKeyframe(SPPath.TIME, 123));
        assertSame(timeline.getPaths().get(1).insert(456), impl.getKeyframe(SPPath.POSITION, 456));
    }

    @Test
    public void testIsTimeKeyframe() {
        impl.getTimePath().insert(123);
        impl.getPositionPath().insert(456);
        impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42);
        assertTrue(impl.isTimeKeyframe(123));
        assertFalse(impl.isTimeKeyframe(456));
        assertFalse(impl.isTimeKeyframe(789));
        assertFalse(impl.isTimeKeyframe(42));
    }

    @Test
    public void testIsPositionKeyframe() {
        impl.getTimePath().insert(123);
        impl.getPositionPath().insert(456);
        impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42);
        assertFalse(impl.isPositionKeyframe(123));
        assertTrue(impl.isPositionKeyframe(456));
        assertFalse(impl.isPositionKeyframe(789));
        assertTrue(impl.isSpectatorKeyframe(42));
    }

    @Test
    public void testIsSpectatorKeyframe() {
        impl.getTimePath().insert(123);
        impl.getPositionPath().insert(456);
        impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42);
        assertFalse(impl.isSpectatorKeyframe(123));
        assertFalse(impl.isSpectatorKeyframe(456));
        assertFalse(impl.isSpectatorKeyframe(789));
        assertTrue(impl.isSpectatorKeyframe(42));
    }

    @Test
    public void testAddPositionKeyframe() {
        assertNull(timeline.peekUndoStack());
        impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
        assertNotNull(timeline.peekUndoStack());
        Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0);
        assertNotNull(keyframe);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
        assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent());
        timeline.undoLastChange();
        assertNull(timeline.peekUndoStack());
        assertNull(impl.getKeyframe(SPPath.POSITION, 0));
    }

    @Test
    public void testAddPositionKeyframeSpectator() {
        assertNull(timeline.peekUndoStack());
        impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, 7);
        assertNotNull(timeline.peekUndoStack());
        Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0);
        assertNotNull(keyframe);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
        assertEquals(keyframe.getValue(SpectatorProperty.PROPERTY), Optional.of(7));
        timeline.undoLastChange();
        assertNull(timeline.peekUndoStack());
        assertNull(impl.getKeyframe(SPPath.POSITION, 0));
    }

    @Test
    public void testPositionKeyframeSpectatorPosition() {
        Keyframe keyframe;
        impl.addTimeKeyframe(0, 0);
        impl.addTimeKeyframe(9, 9);

        impl.addPositionKeyframe(4, 1, 2, 3, 4, 5, 6, 1);
        keyframe = impl.getKeyframe(SPPath.POSITION, 4);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));

        impl.addPositionKeyframe(6, 1, 2, 3, 4, 5, 6, 42);
        keyframe = impl.getKeyframe(SPPath.POSITION, 6);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(12d, 0d, 0d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(0f, 0f, 0f)));

        impl.addPositionKeyframe(8, 1, 2, 3, 4, 5, 6, 42);
        keyframe = impl.getKeyframe(SPPath.POSITION, 8);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(16d, 0d, 0d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(0f, 0f, 0f)));

        impl.addPositionKeyframe(10, 1, 2, 3, 4, 5, 6, 42);
        keyframe = impl.getKeyframe(SPPath.POSITION, 10);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
    }

    @Test(expected = IllegalStateException.class)
    public void testAddPositionKeyframeDuplicate() {
        impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
        impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
    }

    @Test
    public void testUpdatePositionKeyframe() {
        impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
        Change change = impl.updatePositionKeyframe(0, 7, 8, 9, 10, 11, 12);
        assertNotNull(change);
        Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(7d, 8d, 9d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(10f, 11f, 12f)));
        assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent());
        change.undo(timeline);
        keyframe = impl.getKeyframe(SPPath.POSITION, 0);
        assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d)));
        assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f)));
        assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent());
    }

    @Test(expected = IllegalStateException.class)
    public void testUpdatePositionKeyframeNoKeyframe() {
        impl.updatePositionKeyframe(0, 1, 2, 3, 4, 5, 6);
    }

    @Test(expected = IllegalStateException.class)
    public void testUpdatePositionKeyframeSpectatorKeyframe() {
        impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, 7);
        impl.updatePositionKeyframe(0, 1, 2, 3, 4, 5, 6);
    }

    @Test
    public void testRemovePositionKeyframe() {
        impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1);
        impl.removePositionKeyframe(0);
        assertNull(impl.getKeyframe(SPPath.POSITION, 0));
    }

    @Test(expected = IllegalStateException.class)
    public void testRemovePositionKeyframeNoKeyframe() {
        impl.removePositionKeyframe(0);
    }

    @Test
    public void testAddTimeKeyframe() {
        assertNull(timeline.peekUndoStack());
        impl.addTimeKeyframe(0, 1);
        assertNotNull(timeline.peekUndoStack());
        Keyframe keyframe = impl.getKeyframe(SPPath.TIME, 0);
        assertNotNull(keyframe);
        assertEquals(keyframe.getValue(TimestampProperty.PROPERTY), Optional.of(1));
        timeline.undoLastChange();
        assertNull(timeline.peekUndoStack());
        assertNull(impl.getKeyframe(SPPath.TIME, 0));
    }

    @Test(expected = IllegalStateException.class)
    public void testAddTimeKeyframeDuplicate() {
        impl.addTimeKeyframe(0, 1);
        impl.addTimeKeyframe(0, 1);
    }

    @Test
    public void testUpdateTimeKeyframe() {
        impl.addTimeKeyframe(0, 1);
        Change change = impl.updateTimeKeyframe(0, 2);
        assertNotNull(change);
        assertEquals(impl.getKeyframe(SPPath.TIME, 0).getValue(TimestampProperty.PROPERTY), Optional.of(2));
        change.undo(timeline);
        assertEquals(impl.getKeyframe(SPPath.TIME, 0).getValue(TimestampProperty.PROPERTY), Optional.of(1));
    }

    @Test(expected = IllegalStateException.class)
    public void testUpdateTimeKeyframeNoKeyframe() {
        impl.updateTimeKeyframe(0, 1);
    }

    @Test
    public void testRemoveTimeKeyframe() {
        impl.addTimeKeyframe(0, 1);
        impl.removeTimeKeyframe(0);
        assertNull(impl.getKeyframe(SPPath.TIME, 0));
    }

    @Test(expected = IllegalStateException.class)
    public void testRemoveTimeKeyframeNoKeyframe() {
        impl.removeTimeKeyframe(0);
    }

    /**
     * Checks all interpolators to make sure that they are continuous and not fragmented.
     */
    private void assertValidInterpolators(SPPath path, int expectedNumberOfInterpolators) {
        String str = prettyPrintInterpolators(impl, path);
        List<Interpolator> interpolators = impl.getPath(path).getSegments().stream().map(PathSegment::getInterpolator)
                .distinct().collect(Collectors.toList());
        assertFalse("Not all interpolators set: " + str, interpolators.contains(null));
        assertEquals("Mismatched interpolator number: " + str, expectedNumberOfInterpolators, interpolators.size());
        int i = 0;
        for (Interpolator interpolator : interpolators) {
            Keyframe lastKeyframe = null;
            for (PathSegment segment : interpolator.getSegments().stream()
                    .sorted(Comparator.comparing(s -> s.getStartKeyframe().getTime())).collect(Collectors.toList())) {
                String message = "Fragmentation in interpolator " + i + ": " + str;
                assertTrue(message, lastKeyframe == null || segment.getStartKeyframe() == lastKeyframe);
                lastKeyframe = segment.getEndKeyframe();
            }
            i++;
        }
    }

    @Test
    public void testAddPositionKeyframeAppend() {
        addPosition(0, 0);
        addPosition(1, 1);
        addPosition(2, 1);
        addPosition(3, 1);
        addPosition(4, 1);
    }

    @Test
    public void testAddPositionKeyframePrepend() {
        addPosition(4, 0);
        addPosition(3, 1);
        addPosition(2, 1);
        addPosition(1, 1);
        addPosition(0, 1);
    }

    @Test
    public void testAddPositionKeyframeMixed() {
        addPosition(2, 0);
        addPosition(4, 1);
        addPosition(3, 1);
        addPosition(0, 1);
        addPosition(1, 1);
    }

    @Test
    public void testAddPositionSpectatorKeyframe() {
        addPosition(0, 0);
        addSpectator(1, 1);
        addSpectator(2, 2);
        addPosition(4, 3);
        addPosition(7, 3);
        addPosition(8, 3);
        addSpectator(3, 3);
        addSpectator(5, 3);
        addSpectator(6, 5);
    }

    @Test
    public void testRemovePositionKeyframeFromStart() {
        addPosition(0, 0);
        addPosition(1, 1);
        addPosition(2, 1);
        addPosition(3, 1);
        removePosition(0, 1);
        removePosition(1, 1);
        removePosition(2, 0);
        removePosition(3, 0);
    }

    @Test
    public void testRemovePositionKeyframeFromEnd() {
        addPosition(0, 0);
        addPosition(1, 1);
        addPosition(2, 1);
        addPosition(3, 1);
        removePosition(3, 1);
        removePosition(2, 1);
        removePosition(1, 0);
        removePosition(0, 0);
    }

    @Test
    public void testRemovePositionKeyframeFromMiddle() {
        addPosition(0, 0);
        addPosition(1, 1);
        addPosition(2, 1);
        addPosition(3, 1);
        removePosition(1, 1);
        removePosition(2, 1);
        removePosition(0, 0);
        removePosition(3, 0);
    }

    @Test
    public void testRemovePositionSpectatorKeyframe() {
        addPosition(0, 0);
        addPosition(1, 1);
        addPosition(2, 1);
        addSpectator(3, 1);
        addSpectator(4, 2);
        addSpectator(5, 2);
        addPosition(6, 3);
        addPosition(7, 3);
        addSpectator(8, 3);
        addSpectator(9, 4);
        removePosition(4, 4);
        removePosition(9, 3);
        removePosition(6, 3);
        removePosition(7, 2);
        removePosition(0, 2);
        removePosition(1, 2);
        removePosition(3, 2);
        removePosition(2, 1);
        removePosition(5, 0);
        removePosition(8, 0);
    }

    @Test
    public void testAddTimeKeyframeAppend() {
        addTime(0, 0);
        addTime(1, 1);
        addTime(2, 1);
        addTime(3, 1);
        addTime(4, 1);
    }

    @Test
    public void testAddTimeKeyframePrepend() {
        addTime(4, 0);
        addTime(3, 1);
        addTime(2, 1);
        addTime(1, 1);
        addTime(0, 1);
    }

    @Test
    public void testAddTimeKeyframeMixed() {
        addTime(2, 0);
        addTime(4, 1);
        addTime(3, 1);
        addTime(0, 1);
        addTime(1, 1);
    }

    @Test
    public void testRemoveTimeKeyframeFromStart() {
        addTime(0, 0);
        addTime(1, 1);
        addTime(2, 1);
        addTime(3, 1);
        removeTime(0, 1);
        removeTime(1, 1);
        removeTime(2, 0);
        removeTime(3, 0);
    }

    @Test
    public void testRemoveTimeKeyframeFromEnd() {
        addTime(0, 0);
        addTime(1, 1);
        addTime(2, 1);
        addTime(3, 1);
        removeTime(3, 1);
        removeTime(2, 1);
        removeTime(1, 0);
        removeTime(0, 0);
    }

    @Test
    public void testRemoveTimeKeyframeFromMiddle() {
        addTime(0, 0);
        addTime(1, 1);
        addTime(2, 1);
        addTime(3, 1);
        removeTime(1, 1);
        removeTime(2, 1);
        removeTime(0, 0);
        removeTime(3, 0);
    }

    @Test
    public void testSetInterpolator() {
        addPosition(0, 0);
        addPosition(1, 1);
        addPosition(2, 1);
        setInterpolator(1, new LinearInterpolator(), 2);
        assertIsLinear(1);
        Interpolator interpolator = Iterables.get(impl.getPositionPath().getSegments(), 1).getInterpolator();
        assertTrue(interpolator.getKeyframeProperties().contains(CameraProperties.POSITION));
        assertTrue(interpolator.getKeyframeProperties().contains(CameraProperties.ROTATION));
        addPosition(3, 3);
        addPosition(4, 3);
        addPosition(5, 3);
        setInterpolator(3, new LinearInterpolator(), 5);
        assertIsLinear(3);
        removePosition(2, 3);
        removePosition(3, 3);
        removePosition(0, 2);
        removePosition(1, 1);
        removePosition(4, 0);
        removePosition(5, 0);
    }

    private void assertIsLinear(int index) {
        assertTrue("Expected segment " + index + " to have linear interpolator: " + prettyPrintInterpolators(impl, SPPath.POSITION),
                Iterables.get(impl.getPositionPath().getSegments(), index).getInterpolator() instanceof LinearInterpolator);
    }

    @Test(expected = IllegalStateException.class)
    public void testSetInterpolatorNoKeyframe() {
        addTime(0, 0);
        impl.setInterpolator(0, new LinearInterpolator());
    }

    @Test(expected = IllegalStateException.class)
    public void testSetInterpolatorToDefaultNoKeyframe() {
        addTime(0, 0);
        impl.setInterpolatorToDefault(0);
    }

    @Test
    public void testMoveKeyframeSimple() {
        addPosition(0, 0);
        addPosition(1, 1);
        setInterpolator(0, new LinearInterpolator(), 1);
        assertIsLinear(0);

        impl.moveKeyframe(SPPath.POSITION, 0, 2);
        assertValidInterpolators(SPPath.POSITION, 1);
        assertIsLinear(0);
        impl.moveKeyframe(SPPath.POSITION, 2, 0);
        assertValidInterpolators(SPPath.POSITION, 1);
        assertIsLinear(0);

        addPosition(2, 1);
        impl.moveKeyframe(SPPath.POSITION, 0, 3);
        assertValidInterpolators(SPPath.POSITION, 1);
        assertIsLinear(0);
        assertIsLinear(1);
    }

    @Test
    public void testMoveKeyframe() {
        addPosition(1, 0);
        addPosition(3, 1);
        addPosition(5, 1);
        addPosition(7, 1);
        addSpectator(9, 1);
        addSpectator(11, 2);
        addSpectator(13, 2);
        addPosition(15, 3);
        addPosition(17, 3);
        addPosition(19, 3);
        setInterpolator(3, new LinearInterpolator(), 5);
        assertIsLinear(1);
        setInterpolator(5, new LinearInterpolator(), 5);
        assertIsLinear(2);
        // [P 1] C0 [P 3] L1 [P 5] L1 [P 7] C2 [S 9] L3 [S 11] L3 [S 13] C4 [P 15] C4 [P 17] C4 [P 19]

        impl.moveKeyframe(SPPath.POSITION, 5, 0);
        assertValidInterpolators(SPPath.POSITION, 6);
        assertIsLinear(0);
        impl.moveKeyframe(SPPath.POSITION, 0, 5);
        assertValidInterpolators(SPPath.POSITION, 5);
        assertIsLinear(2);

        impl.moveKeyframe(SPPath.POSITION, 3, 12);
        assertValidInterpolators(SPPath.POSITION, 7);
        assertIsLinear(5);
        impl.moveKeyframe(SPPath.POSITION, 12, 3);
        assertValidInterpolators(SPPath.POSITION, 5);
        assertIsLinear(1);

        impl.moveKeyframe(SPPath.POSITION, 7, 14);
        assertValidInterpolators(SPPath.POSITION, 4);
        impl.moveKeyframe(SPPath.POSITION, 14, 20);
        assertValidInterpolators(SPPath.POSITION, 4);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSetDefaultInterpolatorDifferentType() {
        addPosition(0, 0);
        addPosition(1, 1);
        impl.setDefaultInterpolator(new CatmullRomSplineInterpolator(42));
    }

    @Test
    public void testSetDefaultInterpolatorSameType() {
        impl.setDefaultInterpolatorType(InterpolatorType.CATMULL_ROM);
        addPosition(0, 0);
        addSpectator(1, 1);
        addSpectator(2, 2);
        addPosition(3, 3);
        addPosition(4, 3);
        impl.setDefaultInterpolator(new CatmullRomSplineInterpolator(42));
        assertValidInterpolators(SPPath.POSITION, 3);
        assertIsCatmullRom(0, 42);
        assertIsCatmullRom(2, 42);
        assertIsCatmullRom(3, 42);
    }

    private void assertIsCatmullRom(int index, double alpha) {
        String str = prettyPrintInterpolators(impl, SPPath.POSITION);
        Interpolator interpolator = Iterables.get(impl.getPositionPath().getSegments(), index).getInterpolator();
        assertTrue("Expected segment " + index + " to be catmull rom interpolator: " + str,
                 interpolator instanceof CatmullRomSplineInterpolator);
        assertTrue("Expected interpolator of segment segment " + index + " to have alpha " + alpha + ": " + str,
                ((CatmullRomSplineInterpolator) interpolator).getAlpha() == 42);
    }

    private void addPosition(int time, int expectedNumberOfInterpolators) {
        impl.addPositionKeyframe(time, 1, 2, 3, 4, 5, 6, -1);
        assertNotNull(impl.getKeyframe(SPPath.POSITION, time));
        assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
    }

    private void addSpectator(int time, int expectedNumberOfInterpolators) {
        impl.addPositionKeyframe(time, 1, 2, 3, 4, 5, 6, 42);
        assertNotNull(impl.getKeyframe(SPPath.POSITION, time));
        assertTrue(impl.isSpectatorKeyframe(time));
        assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
    }

    private void addTime(int time, int expectedNumberOfInterpolators) {
        impl.addTimeKeyframe(time, 1);
        assertNotNull(impl.getKeyframe(SPPath.TIME, time));
        assertValidInterpolators(SPPath.TIME, expectedNumberOfInterpolators);
    }

    private void removePosition(int time, int expectedNumberOfInterpolators) {
        impl.removePositionKeyframe(time);
        assertNull(impl.getKeyframe(SPPath.POSITION, time));
        assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
    }

    private void removeTime(int time, int expectedNumberOfInterpolators) {
        impl.removeTimeKeyframe(time);
        assertNull(impl.getKeyframe(SPPath.TIME, time));
        assertValidInterpolators(SPPath.TIME, expectedNumberOfInterpolators);
    }

    private void setInterpolator(int time, Interpolator interpolator, int expectedNumberOfInterpolators) {
        impl.setInterpolator(time, interpolator);
        assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators);
    }

    private void assertPositionState(String expected) {
        assertEquals(expected, prettyPrintInterpolators(impl, SPPath.POSITION));
    }

    // Tracks only entity 42 which always is at x=time*2, y,z=0
    private class EntityPositionTrackerMock extends EntityPositionTracker {
        public EntityPositionTrackerMock() {
            super(null);
        }

        @Override
        public Location getEntityPositionAtTimestamp(int entityID, long timestamp) {
            if (entityID == 42) {
                return new Location(timestamp * 2, 0 ,0, 0, 0);
            } else {
                return null;
            }
        }
    }

    public static String prettyPrintInterpolators(SPTimeline timeline, SPPath spPath) {
        return prettyPrintInterpolators(timeline, spPath, Collections.emptyMap());
    }

    public static String prettyPrintInterpolators(SPTimeline timeline, SPPath spPath, Map<PathSegment, Interpolator> updates) {
        Map<Interpolator, String> interpolatorIdMap = new IdentityHashMap<>();
        Path path = timeline.getPath(spPath);
        StringBuilder sb = new StringBuilder();
        for (Keyframe keyframe : path.getKeyframes()) {
            if (spPath == SPPath.TIME) {
                sb.append("[T ").append(keyframe.getTime()).append("]");
            } else if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) {
                sb.append("[S ").append(keyframe.getTime()).append("]");
            } else {
                sb.append("[P ").append(keyframe.getTime()).append("]");
            }
            path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().ifPresent(segment -> {
                Interpolator interpolator = updates.getOrDefault(segment, segment.getInterpolator());
                String id = interpolatorIdMap.computeIfAbsent(interpolator, i ->
                        (i instanceof LinearInterpolator ? "L" : "C") + interpolatorIdMap.size());
                sb.append(' ').append(id).append(' ');
            });
        }
        return sb.toString();
    }
}
