/*
 * Semitone - tuner, metronome, and piano for Android
 * Copyright (C) 2019  Andy Tockman <[email protected]>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package mn.tck.semitone;

import android.content.Context;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Gravity;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.SeekBar;

import androidx.core.content.ContextCompat;

import java.util.ArrayList;

public class MetronomeFragment extends SemitoneFragment {

    final int MIN_TEMPO = 40;
    final int MAX_TEMPO = 400;

    final int TAPS_MAX = 10;
    long[] taps;
    int ntaps;

    int tempo, beats, subdiv;
    boolean enabled;

    LinearLayout dotsView;
    ArrayList<Dot> dots;
    int activeDot;

    NumBox tempoBox, beatsBox, subdivBox;
    SeekBar tempoBar;
    Button startBtn, tapBtn;

    View view;
    ShapeDrawable dotOn, dotOnBig, dotOff, dotOffBig;
    LinearLayout.LayoutParams dotParams;

    Tick tick;
    int strong, weak;

    public MetronomeFragment() {
        MainActivity.mf = this;
    }

    @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
        return inflater.inflate(R.layout.metronome, container, false);
    }

    @Override public void onViewCreated(View view, Bundle state) {
        this.view = view;

        tempo = 120; beats = 4; subdiv = 1; enabled = false;
        tempoBox = (NumBox) view.findViewById(R.id.tempo);
        beatsBox = (NumBox) view.findViewById(R.id.beats);
        subdivBox = (NumBox) view.findViewById(R.id.subdiv);
        tempoBar = (SeekBar) view.findViewById(R.id.tempobar);
        startBtn = (Button) view.findViewById(R.id.start);
        tapBtn = (Button) view.findViewById(R.id.tap);
        dotsView = (LinearLayout) view.findViewById(R.id.dots);

        tempoBox.cb = new NumBox.Callback() {
            @Override public void onChange(int val) {
                tempo = val;
                tempoBar.setProgress(tempo - MIN_TEMPO);
                intermediateTempoChange();
            }
        };
        tempoBar.setProgress(tempo - MIN_TEMPO);

        beatsBox.cb = new NumBox.Callback() {
            @Override public void onChange(int val) {
                beats = val;
                intermediateBeatChange();
            }
        };

        subdivBox.cb = new NumBox.Callback() {
            @Override public void onChange(int val) {
                subdiv = val;
                intermediateTempoChange();
            }
        };

        tempoBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override public void onProgressChanged(SeekBar sb, int val, boolean fromUser) {
                if (!fromUser) return;
                tempo = val + MIN_TEMPO;
                tempoBox.setValue(tempo);
                intermediateTempoChange();
            }
            @Override public void onStartTrackingTouch(SeekBar sb) {}
            @Override public void onStopTrackingTouch(SeekBar sb) {}
        });

        startBtn.setOnClickListener(new Button.OnClickListener() {
            @Override public void onClick(View v) { toggle(); }
        });

        taps = new long[TAPS_MAX];
        ntaps = 0;
        tapBtn.setOnClickListener(new Button.OnClickListener() {
            @Override public void onClick(View v) {
                long time = System.currentTimeMillis();
                if (ntaps > 0 && time - taps[ntaps-1] > 3000) {
                    // time out after 3 seconds
                    ntaps = 1;
                    taps[0] = time;
                } else if (ntaps == TAPS_MAX) {
                    for (int i = 0; i < TAPS_MAX-1; ++i) taps[i] = taps[i+1];
                    taps[TAPS_MAX-1] = time;
                } else {
                    taps[ntaps++] = time;
                }

                if (ntaps > 1) {
                    tempo = (int)(60000*(ntaps-1) / (taps[ntaps-1] - taps[0]));
                    tempoBox.setValue(tempo);
                    tempoBar.setProgress(tempo - MIN_TEMPO);
                    intermediateTempoChange();
                }
            }
        });

        final int smallSize = getResources().getDimensionPixelSize(R.dimen.small_dot),
              largeSize = getResources().getDimensionPixelSize(R.dimen.large_dot);
        dotOn     = makeDot(smallSize, R.color.white);
        dotOnBig  = makeDot(largeSize, R.color.white);
        dotOff    = makeDot(smallSize, R.color.grey1);
        dotOffBig = makeDot(largeSize, R.color.grey1);

        dotParams = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1);

        dots = new ArrayList<Dot>();
        intermediateBeatChange();
    }

    @Override public void onDestroyView() {
        super.onDestroyView();
        if (tick != null) {
            tick.keepGoing = false;
            tick.interrupt();
        }
    }

    @Override public void onSettingsChanged() {}

    private ShapeDrawable makeDot(int size, int color) {
        ShapeDrawable dot = new ShapeDrawable(new OvalShape());
        dot.setIntrinsicWidth(size);
        dot.setIntrinsicHeight(size);
        dot.getPaint().setColor(ContextCompat.getColor(getContext(), color));
        return dot;
    }

    private void toggle() {
        enabled = !enabled;
        if (enabled) {
            startBtn.setText(getString(R.string.stop_btn));
            activeDot = -1;
            tick = new Tick(tempo, subdiv);
            tick.start();
            getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        } else {
            startBtn.setText(getString(R.string.start_btn));
            if (tick != null) {
                tick.keepGoing = false;
                tick.interrupt();
            }
            getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
    }

    private void intermediateTempoChange() {
        if (!enabled) return;
        long elapsedTime = System.currentTimeMillis() - tick.tickTime(tick.nTicks - 1);

        tick.tempo = tempo;
        tick.subdiv = subdiv;
        if (elapsedTime >= tick.delayTime()) {
            // immediate tick
            tick.nTicks = 0;
            tick.startTime = System.currentTimeMillis();
            tick.nextTime = tick.startTime;
        } else {
            // count the time since the last tick towards the next one
            tick.nTicks = 1;
            tick.startTime = System.currentTimeMillis() - elapsedTime;
            tick.nextTime = tick.tickTime(1);
        }

        // break out of any sleeps currently happening
        tick.interrupt();
    }

    private void intermediateBeatChange() {
        for (Dot dot : dots) {
            dotsView.removeView(dot);
        }
        dots.clear();

        for (int i = 0; i < beats; ++i) {
            Dot dot = new Dot(getContext(), i == 0);
            dotsView.addView(dot);
            dots.add(dot);
        }

        if (enabled && activeDot > beats-1) activeDot = beats-1;
    }

    class Tick extends Thread {
        protected int tempo, subdiv, nTicks;
        protected long startTime, nextTime;
        boolean keepGoing;
        public Tick(int tempo, int subdiv) {
            this.tempo = tempo;
            this.subdiv = subdiv;
            keepGoing = true;
        }

        @Override public void run() {
            nTicks = 0;
            startTime = System.currentTimeMillis();
            nextTime = startTime;
            while (keepGoing) {
                long diff = nextTime - System.currentTimeMillis();
                if (diff <= 0) {}
                // else if (diff <= 5) {
                //     // 5ms - arbitrary cutoff for when to busyloop
                //     while (System.currentTimeMillis() < nextTime);
                // }
                else {
                    // we have a while - sleep and check again
                    try { Thread.sleep(diff); } catch (InterruptedException e) {}
                    continue;
                }

                if (PianoEngine.paused) {
                    // the app was backgrounded and the setting to keep ticking
                    // is off, so stop the metronome
                    if (getActivity() != null) getActivity().runOnUiThread(new Runnable() {
                        @Override public void run() {
                            toggle();
                        }
                    });
                    break;
                }

                // time for another tick
                if (nTicks % subdiv == 0) activeDot = (activeDot + 1) % beats;
                PianoEngine.playFile(nTicks % subdiv == 0 && dots.get(activeDot).big ?
                        "strong.mp3" : "weak.mp3", 440);
                if (getActivity() != null) getActivity().runOnUiThread(new Runnable() {
                    @Override public void run() {
                        dots.get(activeDot).turnOn();
                    }
                });
                try { Thread.sleep(Math.min(100, (long)(delayTime()/2))); } catch (InterruptedException e) {}
                if (getActivity() != null) getActivity().runOnUiThread(new Runnable() {
                    @Override public void run() {
                        dots.get(activeDot).turnOff();
                    }
                });

                // queue the next tick
                nextTime = tickTime(++nTicks);
            }
        }

        protected long tickTime(int nTick) {
            return startTime + Math.round(nTick * delayTime());
        }

        protected double delayTime() { return 1000 * 60.0 / (tempo*subdiv); }
    }

    class Dot extends ImageView {
        boolean big;
        public Dot(Context context, boolean big) {
            super(context);
            this.big = big;
            turnOff();
            setLayoutParams(dotParams);
        }

        @Override public boolean onTouchEvent(MotionEvent ev) {
            if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
                big = !big;
                turnOff();
                return true;
            }
            return false;
        }

        public void turnOff() { setImageDrawable(big ? dotOffBig : dotOff); }
        public void turnOn()  { setImageDrawable(big ? dotOnBig  : dotOn);  }
    }

}