package hh.michelson;

import android.content.Context;
import android.graphics.Point;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.WindowManager;
import android.widget.ListView;
import android.widget.SeekBar;

import hh.lib.D;
import hh.lib.DA;
import hh.ui.FlatButton;

public class A extends DA implements WavesFragment.OnFragmentInteractionListener, Handler.Callback {
    /**
     * ----------------------------------------------------------
     * main activity template with uninstall in menu
     * ----------------------------------------------------------
     */

    WavesFragment waveList;
    boolean playing = C.playingDefault;
    Wave wave = null;
    double phase = 0;
    boolean node = true;

	volatile double freq = C.freqDef.Default;
	volatile double freqProgr = C.freqProgrDef.Default;
	volatile double targetFreq, deltaFreq, dt;
	final static int UPDFREQ = 0;

    double amplitude;
    volatile double amplKoef = 0;
    double maxY = 0;
    boolean mute = false;

	double pulseSecs = C.pulseDef.Default;
	final double pulseStep = (Math.log10(C.pulseDef.Max) - Math.log10(C.pulseDef.Min)) / C.maxBarProgress;    /* in secs */
    int pulseCycles;     /* pulse in cycles */
	boolean isInPulse = true;

	double pauseSecs = C.pauseDef.Default;
	final double pauseStep = (Math.log10(C.pauseDef.Max) - Math.log10(C.pauseDef.Min)) / C.maxBarProgress;    /* in secs */
    int pauseCycles;     /* pause in cycles */
    volatile double pulsePausePhase = 0;

	double slope = C.slopeDef.Default;
    final double slopeStep = (Math.log10(C.slopeDef.Max) - Math.log10(C.slopeDef.Min)) / C.maxBarProgress;
    double slopeKoef = 1d;
	final static int SETSLOPE = 1;

	Handle amplBar;
	Handle pulseBar;
	Handle pauseBar;
	Handle slopeBar;
	Handle freqBar;
	Handle freqProgressBar;
	Handle[] handles;

	AudioTrack audioTrack;
	int buffsize;
	class Samples {
		short[] samples;
		boolean ready;

		Samples(int n) {
			samples = new short[n];
		}
	}
	Samples[] s;
	Thread audioThread = null;
	boolean audioIsActive = true;

	Handler h = new Handler(this);

	class AmplHandle extends Handle {

		AmplHandle(D d, double actual) { super(d, C.amplitudeDef, actual); }

		@Override
		public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
			super.onProgressChanged(bar, progress, fromUser);
			getAmpl();
		}

		@Override
		public boolean onLongClick(View v) {
			final boolean r = super.onLongClick(v);
			getAmpl();
			return r;
		}
	}

	class PulseHandle extends Handle {

		PulseHandle(D d, double actual) { super(d, C.pulseDef, actual); }

		@Override
		double progress2value(int progress) { return progress == 0 ? 0d : Math.pow(10, progress * pulseStep - 1); }

		@Override
		int value2progress(double value) { return value == 0 ? 0 : (int)((Math.log10(value) + 1) / pulseStep); }
	}

	class PauseHandle extends Handle {

		PauseHandle(D d, double actual) { super(d, C.pauseDef, actual); }

		PauseHandle(D d, String label, double min, double max, double def, double actual, String key) {
			super(d, label, min, max, def, actual, key);
		}

		@Override
		double progress2value(int progress) { return progress == 0 ? 0d : Math.pow(10, progress * pulseStep - 1); }

		@Override
		int value2progress(double value) { return value == 0 ? 0 : (int)((Math.log10(value) + 1) / pauseStep); }
	}

	class SlopeHandle extends Handle {

		SlopeHandle(D d, double actual) { super(d, C.slopeDef, actual); }

		SlopeHandle(D d, String label, double min, double max, double def, double actual, String key) {
			super(d, label, min, max, def, actual, key);
		}

		@Override
		double progress2value(int progress) { return progress == 0 ? 0d : Math.pow(10, (progress + 1) * slopeStep); }

		@Override
		int value2progress(double value) { return value == 0 ? 0 : (int)(Math.log10(value)/slopeStep - 1); }
	}

	@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

		setContentView(R.layout.main);

	    Point size = new Point();
	    ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(size);
	    C.windowWidth = size.x;

		getValues();
	    setMenu();
	    findViewById(R.id.dispWave).setOnClickListener(new View.OnClickListener() {
		    public void onClick(View v) {
			    showListUI();
		    }
	    });
		setControlHandles();
	    findViewById(R.id.mute).setOnClickListener(new View.OnClickListener() {
		    public void onClick(View v) {
			    flipMute();
		    }
	    });
	    setMute();
	    allocAudio();

		if(savedInstanceState != null) playing = savedInstanceState.getBoolean(C.playingKey, C.playingDefault);
	    if(playing)startAudio();
	    else showListUI();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) { outState.putBoolean(C.playingKey, playing); }

    @Override
    public void onDestroy() {
        super.onDestroy();
	    stopAudio();
	    audioTrack.release();
    }

    @Override
	public boolean handleMessage(Message msg) {
	    switch(msg.what) {
		    case UPDFREQ:
			    updateFreq();
			    return true;
		    case SETSLOPE:
			    setSlopeBar();
			    return true;
		    default:
			    return d.handleMessage(msg);
	    }
    }

	@Override
	public void setNewWave(int position) {
		if(fm.getBackStackEntryCount() > 0) {
			fm.popBackStackImmediate();
			fm.beginTransaction().detach(waveList).commit();
		}
		if(position >= 0) {
			wave = Enum.valueOf(Wave.class, Wave.names[position]);
			sp.edit().putString(C.waveKey, wave.name()).apply();
		}
		playing = true;
		startAudio();
	}

	void startAudio() {
	    wave.init(this);
		if(audioThread != null) return;
		audioThread = new Thread() {
			public void run() {
	            setPriority(Thread.MAX_PRIORITY);
				play();
			}
		};
		s[0].ready = false;
		s[1].ready = false;
		audioTrack.play();
		audioThread.start();
		audioIsActive = true;

		new Thread() { public void run() { synthesize(); } }.start();
	}

	void stopAudio() {
		if(audioThread == null) return;
		audioIsActive = false;
		try { audioThread.join(); } catch (InterruptedException e) { e.printStackTrace(); }
		audioThread = null;
		audioTrack.stop();
	}

	void getValues() {
		wave = Enum.valueOf(Wave.class, sp.getString(C.waveKey, C.waveDefault.name()));
		amplitude = sp.getFloat(C.amplitudeDef.Key, Double.valueOf(C.amplitudeDef.Default).floatValue());
		mute = sp.getBoolean(C.muteKey, C.muteDefault);
		pulseSecs = sp.getFloat(C.pulseDef.Key, Double.valueOf(C.pulseDef.Default).floatValue());
		pauseSecs = sp.getFloat(C.pauseDef.Key, Double.valueOf(C.pauseDef.Default).floatValue());
		slope = sp.getFloat(C.slopeDef.Key, Double.valueOf(C.slopeDef.Default).floatValue());
		freq = sp.getFloat(C.freqDef.Key, Double.valueOf(C.freqDef.Default).floatValue());
		if(freq < C.freqDef.Min) freq = C.freqDef.Min;
		freqProgr = sp.getFloat(C.freqProgrDef.Key, Double.valueOf(C.freqProgrDef.Default).floatValue());
	}

    void setControlHandles() {
	    amplBar = new AmplHandle(d, amplitude);
        pulseBar = new PulseHandle(d, pulseSecs);
	    pauseBar = new PauseHandle(d, pauseSecs);
	    slopeBar = new SlopeHandle(d, slope);
	    freqBar = new Handle(d, C.freqDef, freq);
	    freqProgressBar = new Handle(d, C.freqProgrDef, freqProgr);

	    handles = new Handle[] { amplBar, pulseBar, pauseBar, slopeBar, freqBar, freqProgressBar };
	    ((ListView)findViewById(R.id.handles)).setAdapter(new Windrover(d, "Controls", this, R.layout.handles_row, handles));
    }

	void showListUI() {
		stopAudio();
		playing = false;
		waveList = new WavesFragment();
		fm.beginTransaction()
				.add(R.id.main, waveList)
				.addToBackStack(null)
				.commit();
	}

	void getAmpl() {
		synchronized(this) {
			amplitude = amplBar.getActual();
			setAmplKoef();
		}
    }

    void setAmplKoef() {
        amplKoef = amplitude;
        if(maxY > 1d) amplKoef = amplKoef / maxY;
    }

    void setMaxY(double maxY) {
		this.maxY = maxY;
		setAmplKoef();
	}

	void updateFreq() {
        /* zatím se volá z nitě syntetizátoru (netřeba synchronizovat), který zajišťuje pravidelný rytmus */
		double nextTargFreq = targetFreq;

		final double barFreq = freqBar.getActual();
        final double fProgress = freqProgressBar.getActual();

        if(targetFreq != barFreq) nextTargFreq = barFreq;     // změna freqBar má přednost
        else if(fProgress != 0d) {
        	nextTargFreq = targetFreq + fProgress;
            if(nextTargFreq > C.freqDef.Max) nextTargFreq = C.freqDef.Max;
            if(nextTargFreq < C.freqDef.Min) nextTargFreq = C.freqDef.Min;
			freqBar.setActual(nextTargFreq);
        }

		if(nextTargFreq != targetFreq) {
        	synchronized(this) {
        		targetFreq = nextTargFreq;
		        deltaFreq = (targetFreq - freq) / 10;
	        }
			if(ll(5)) l(String.format(lo, "freq=%f, targetFreq=%f, deltaFreq=%f",
					freq, targetFreq, deltaFreq));
		}
    }

	void getPulse() {
		pulseCycles = dur2len(pulseBar.getActual());
		pauseCycles = dur2len(pauseBar.getActual());
		Message.obtain(h, SETSLOPE).sendToTarget();
	}

    void setSlopeBar() {
	    /* slope can't exceed pulse/2 */
        int p = (int)Math.floor(pulseCycles /2);
        if(pulseCycles > 0 && slopeBar.getActual() > p) slopeBar.setActual(p);
	}

    int dur2len(double duration) { return (int)Math.round(duration * freq); }

    void flipMute() {
        mute = !mute;
        setMute();
    }

	void setMute() {
		((FlatButton)findViewById(R.id.mute)).setText(mute ? "unmute" : "mute");
		sp.edit().putBoolean(C.muteKey, mute).apply();
	}

	void allocAudio() {
		final int audioSessionId = ((AudioManager)getSystemService(Context.AUDIO_SERVICE)).generateAudioSessionId();
		buffsize = AudioTrack.getMinBufferSize(
				C.SR,
				AudioFormat.CHANNEL_OUT_MONO,
				AudioFormat.ENCODING_PCM_16BIT);
		l(4, String.format(lo, "buf size=%d", buffsize));
		audioTrack = new AudioTrack(
				new AudioAttributes.Builder()
						.setUsage(AudioAttributes.USAGE_UNKNOWN)
						.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
						.build(),
				new AudioFormat.Builder()
						.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
						.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
						.build(),
				buffsize,
				AudioTrack.MODE_STREAM,
				audioSessionId);
		s = new Samples[] {new Samples(buffsize), new Samples(buffsize)};
	}

	void play() {
		while(audioIsActive) {
			try {
				playSamples(0);
				if(audioIsActive) playSamples(1);
			} catch(InterruptedException e) {}
		}
		l(4,"exiting playing thread");
	}

	void playSamples(int y) throws InterruptedException {
		synchronized(s[y]) {
			while(!s[y].ready && audioIsActive) {
				if(ll(7)) l("waiting for samples " + y);
				s[y].wait(555);
			}
		}
		if(!audioIsActive) return;
		if(ll(7)) l(String.format(lo, "playing samples %d", y));
		int offset = 0, size = buffsize, put;
		while((put = audioTrack.write(s[y].samples, offset, size)) < size) {
			offset += put;
			size -= put;
		}
		synchronized(s[y]) {
			s[y].ready = false;
			s[y].notify();
		}
	}

    void synthesize() {
	    phase = 0;
	    freq = targetFreq = freqBar.getActual();
	    dt = C.twoPI * freq / C.SR;
	    while(audioIsActive) {
		    try {
			    compute(0);
		        if(audioIsActive) compute(1);
		    } catch(InterruptedException e) {}
		    // pravidelný interval pro snímání změny frekvence; samotná změna frekvence s provede postupně, když je průběh vlny v uzlu
		    Message.obtain(h, UPDFREQ).sendToTarget();
	    }
    }

    void compute(int i) throws InterruptedException {
	    synchronized(s[i]) {
		    while(s[i].ready && audioIsActive) s[i].wait();
	    }
		if(!audioIsActive) return;
	    computeSamples(i);
	    synchronized(s[i]) {
		    s[i].ready = true;
		    s[i].notify();
	    }
    }

	void computeSamples(int i) {
		for(int j = 0; j < buffsize; j++) {
			synchronized(this) {    // každý vzorek se synchronizuje zvlášť
				if(node) adjustInNode();
				final double k = (mute ? 0 : 1) * slopeKoef * amplKoef;

				s[i].samples[j] = (k != 0 ? (short)(k * wave.value(phase)) : 0);

				phase += dt;
				if(phase > C.twoPI) {
					phase -= C.twoPI;
					node = true;
				}
				else node = false;

				if(node) {
					++pulsePausePhase;
					if(pulsePausePhase > (pulseCycles + pauseCycles)) pulsePausePhase = 0;
				}
			}
		}
	}

	void adjustInNode() {
		getPulse();
		if(pauseCycles == 0) isInPulse = true;
		else isInPulse = ((pulseCycles != 0) && (pulsePausePhase <= pulseCycles));
		if(isInPulse) {
			slopeKoef = 1d;
			setSlopeBar();
			final double slope = slopeBar.getActual();
			if(slope > 0 && pauseCycles > 0) {
				if(pulsePausePhase < slope)
					slopeKoef = (pulsePausePhase + 1d) / (slope + 1d);
				if(pulsePausePhase > (pulseCycles - 1 - slope))
					slopeKoef = (pulseCycles - pulsePausePhase) / (slope + 1d);
			}
		}
		else slopeKoef = 0;

		if(freq != targetFreq) {
			if(Math.abs(freq - targetFreq) < Math.abs(deltaFreq)) freq = targetFreq;
			else freq += deltaFreq;
			dt = C.twoPI * freq / C.SR;
			getPulse();
		}
	}
}