package de.duehl.swing.ui.elements.slider;

/*
 * Copyright 2025 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Hashtable;

import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;

import de.duehl.basics.datetime.time.TimeHelper;
import de.duehl.swing.ui.elements.slider.data.UpdateRunnable;
import de.duehl.swing.ui.elements.slider.data.TimeSliderUser;

/**
 * Diese Klasse stellt einen JSlider dar, welcher sich im Laufe der Zeit automatisch weiterbewegt.
 *
 * @version 1.01     2025-07-25
 * @author Christian Dühl
 */

public class TimeRunningSlider {

    /** Die Klasse, welche diesen Slider verwendet. */
    private TimeSliderUser sliderUser;

    /** Der Slider der den Ablauf der Zeit anzeigt. */
    private final JSlider slider;

    /** Die Zeit in Millisekunden, zu der gestartet wurde. */
    private long millisStarted;

    /** Die Anzahl Millisekunden zwischen Start und Stopp. */
    private long deltaMillis;

    /**
     * Die Zeit in Millisekunden, zu der der Slider am Ende ankommen und der Aktualisierungsthread
     * beendet werden soll.
     */
    private long millisToStop;

    /** Die Zeit in Millisekunden, zu der der Slider pausiert wurde. */
    private long millisPausedAt;

    /** Das Runnable, das die Aktualisierung behandelt. */
    private UpdateRunnable updateRunnable;

    /** Die Laufzeit in Sekunden, die vor dem Start schon abgelaufen war. */
    private int secondsPlayedBeforeStart;

    /** Der Wert den der Slide zu Beginn haben soll. */
    private int sliderStartValue;

    /**
     * Gibt an, ob Veränderungen am Slider ignoriert werden sollen, weil sie von dieser Klasse
     * selbst vorgenommen werden.
     */
    private volatile boolean ignoreSliderChanges;

    /** Gibt an, ob die aktuelle Zeit als Tooltip angezeigt wird. */
    private boolean showActualTimeAsToolTip;

    /** Konstruktor. */
    public TimeRunningSlider(TimeSliderUser sliderUser) {
        this.sliderUser = sliderUser;
        ignoreSliderChanges = false;
        showActualTimeAsToolTip = false;

        slider = new JSlider();

        initSlider();
    }

    private void initSlider() {
        slider.setMinimum(0);
        slider.setMaximum(0);
        slider.setValue(0);
        slider.setPaintTicks(false);
        slider.setSnapToTicks(false);

        slider.setMajorTickSpacing(1 * 1000); // muss man so fein setzen, sonst bewegt sich der
                                              // Slider nicht kontinuierlich.
        slider.setMinorTickSpacing(15 * 1000); // egal

        //slider.setPaintTrack(true);
        slider.addChangeListener(e -> sliderChangedStarted());
        slider.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent event) {
                sliderChanged();
            }
        });
    }

    /**
     * Wird aufgerufen, wenn der Benutzer mit dem Slider interagiert und die Mus losgelassen hat.
     */
    private void sliderChangedStarted() {
        if (ignoreSliderChanges) {
            return;
        }
        if (!sliderUser.isPaused()) {
            sliderUser.pause();
        }
    }

    /** Wird aufgerufen, wenn der Benutzer mit dem Slider interagiert. */
    private void sliderChanged() {
        sliderUser.resume(slider.getValue(), slider.getMaximum());
    }

    /** Legt fest, dass die aktuelle Zeit als Tooltip angezeigt wird. */
    public void showActualTimeAsToolTip() {
        showActualTimeAsToolTip = true;
    }

    /** Legt fest, dass kein Tooltip angezeigt wird. */
    public void showNoToolTip() {
        showActualTimeAsToolTip = false;
    }

    /**
     * Startet den Slider.
     *
     * @param secondsPlayedBeforeStart
     *            Die Laufzeit in Sekunden, die vor dem Start schon abgelaufen war.
     * @param secondsToPlay
     *            Die Laufzeit ab jetzt in Sekunden.
     * @param sliderStartValue
     *            Der Wert den der Slide zu Beginn haben soll.
     */
    public void start(int secondsPlayedBeforeStart, int secondsToPlay, int sliderStartValue) {
        stopActualisationRunnable();

        this.secondsPlayedBeforeStart = secondsPlayedBeforeStart;
        this.sliderStartValue = sliderStartValue;
        millisStarted = System.currentTimeMillis();
        deltaMillis = 1000L * secondsToPlay;
        millisToStop = millisStarted + deltaMillis;
        int maximum = (int) deltaMillis;
        ignoreSliderChanges = true;
        slider.setMaximum(maximum);
        slider.setValue(sliderStartValue);
        ignoreSliderChanges = false;

        String minutesSeconds = TimeHelper.secondsToMinutesSeconds(secondsToPlay);
        Hashtable<Integer, JLabel> labelTable = new Hashtable<>();
        labelTable.put(Integer.valueOf(0), new JLabel("0"));
        labelTable.put(Integer.valueOf(maximum), new JLabel(minutesSeconds));

        slider.setLabelTable(labelTable);
        slider.setPaintLabels(true);

        createRunnableAndStartActualisationThread();
    }

    /** Erstellt ein neues Runnable-Objekt und startet es in einem Thread. */
    private void createRunnableAndStartActualisationThread() {
        long actualisationTimeMillis = 10l;
        updateRunnable = new UpdateRunnable(() -> updateSlider(),
                actualisationTimeMillis);
        Thread thread = new Thread(updateRunnable);
        thread.start();
    }

    private void updateSlider() {
        SwingUtilities.invokeLater(() -> updateSliderInEdt());
    }

    private synchronized void updateSliderInEdt() {
        long millisNow = System.currentTimeMillis();
        if (millisNow >= millisToStop) {
            ignoreSliderChanges = true;
            slider.setValue(slider.getMaximum());
            ignoreSliderChanges = false;
            stop();
        }
        else {
            long millisFromStart = millisNow - millisStarted;
            ignoreSliderChanges = true;
            slider.setValue((int) millisFromStart + sliderStartValue);
            ignoreSliderChanges = false;
            if (showActualTimeAsToolTip) {
                showActualTimeAsToolTip(millisFromStart);
            }
        }
    }

    private void showActualTimeAsToolTip(long millisFromStart) {
        int seconds = (int) (millisFromStart / 1000) + secondsPlayedBeforeStart;
        String minutesSeconds = TimeHelper.secondsToMinutesSeconds(seconds);
        slider.setToolTipText(minutesSeconds);
    }

    /** Hält den Slider an. */
    public void stop() {
        stopActualisationRunnable();
    }

    /** Pausiert den Slider. */
    public void pause() {
        millisPausedAt = System.currentTimeMillis();
        updateRunnable.stop();
    }

    /** Setzte den Slider nach einer Pause fort. */
    public void resume() {
        long millisPauseEndAt = System.currentTimeMillis();
        long pauseDelta = millisPauseEndAt - millisPausedAt;
        millisStarted += pauseDelta;
        millisToStop += pauseDelta;
        createRunnableAndStartActualisationThread();
    }

    /** Hält das Runnable-Objekt an. */
    private void stopActualisationRunnable() {
        if (null != updateRunnable) {
            updateRunnable.stop();
        }
    }

    /** Getter für den Slider der den Ablauf der Zeit anzeigt. */
    public JSlider getSlider() {
        return slider;
    }

    /** Gibt den Wert des Slides zurück. */
    public int getSliderValue() {
        return slider.getValue();
    }

    /** Setzt den Wert des Slides zurück. */
    public void setSliderValue(int value) {
        slider.setValue(value);
    }

    /** Gibt die bis jetzt gespielten Sekunden zurück. */
    public int getSecondsPlayed() {
        long millisNow = System.currentTimeMillis();
        long millisFromStart = millisNow - millisStarted;
        int seconds = (int) (millisFromStart / 1000) + secondsPlayedBeforeStart;
        return seconds;
    }

}
