/*
 * Copyright 2015 Andrew T. Flowers, K0SM
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dopplerpsk.dsp;

import dopplerpsk.event.ISatelliteEventSink;
import dopplerpsk.event.SatelliteUpdateEvent;
import java.nio.ByteBuffer;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.SourceDataLine;

/**
 * The PSK31 Transmitter.
 * 
 * @author Andrew T. Flowers
 */
public class Psk31Transmitter implements ISatelliteEventSink {

    private final SourceDataLine line;
    private final AudioFormat format;
    private final float sampleRate;

    private final Deque<Character> txQueue;

    private final Mixer m;
    private final DbpskDDS dds;
    private final Object charLock;
    private AudioThread audioThread;
    private boolean isTx;
    private final Set<ITxListener> listeners;
    
    private final double baudrate = 31.25; // for PSK31
    private final LocalOscillator lo;
    private Object lastName;
    private boolean isLoop;
    private String loopMessage;

    /**
     * Creates a new PSK31 transmitter.
     * @param line
     * @param format
     * @param dopplerCenter 
     */
    public Psk31Transmitter(SourceDataLine line, AudioFormat format, double dopplerCenter) {
        charLock = new Object();
        this.listeners = new HashSet();
        this.line = line;
        this.format = format;
        sampleRate = format.getSampleRate();
        this.lo = new LocalOscillator(format.getSampleRate(), dopplerCenter);
        m = new Mixer(lo);
        dds = new DbpskDDS(sampleRate, baudrate);

        txQueue = new LinkedList(); // stores the characters for transmission, FIFO
        startAudioThread();
    }
    
    public synchronized void addListener(ITxListener l) {
        this.listeners.add(l);
    }
    
    public synchronized void removeListener(ITxListener l) {
        this.listeners.remove(l);
    }
    private void startAudioThread() {
        line.start();
        if (audioThread != null) {
            audioThread.kill();
        }
        audioThread = new AudioThread();
        audioThread.start();
    }
    
    public void stopAudioThread() {
       if (audioThread != null) {
            audioThread.kill();
        }
    }
    
    
    public void startTx() {
        isTx = true;
    }

    public void stopTx() {
        isTx = false;
    }

    /**
     * Backspace--removes last element added to the TX queue.
     */
    public void removeChar() {
        synchronized (charLock) {
            if (!txQueue.isEmpty()) {
                txQueue.removeLast();
            }
        }
    }

    public void putChar(char c) {
        synchronized (charLock) {
            txQueue.add(c);
        }
    }

    public void putString(String s) {
        for (char c : s.toCharArray()) {
            putChar(c);
        }
    }

    public void clearBuffer() {
         synchronized (charLock) {
            txQueue.clear();
        }
    }

    @Override
    public void SatelliteUpdateEventOccured(SatelliteUpdateEvent evt) {
        String name = evt.getName();
        if (!name.equals(lastName)) {
            // Satellite changed.  Adjust our carrier for absolute Doppler
            // shift from the doppler center freq.
            lo.setInstantaneousFrequency(lo.getCenterFrequency() + evt.getDopplerShift());
            lastName = name;
        }
        
        // if the TX isn't on, we update the carrier frequency directly.  This
        // keeps absolute frequency error from accumulating over long periods
        // when the TX isn't on.
        if (!isTx) {
            lo.setInstantaneousFrequency(lo.getCenterFrequency() + evt.getDopplerShift());
        }
        // now update the LO to track the new doppler rate.
        lo.adjustCarrierFrequency(evt.getDopplerShift(), evt.getDopplerRate());
    }

    public double getCenterFrequency() {
       return lo.getCenterFrequency();
    }

    public double getInstantaneousFrequency() {
       return lo.getInstantaneousFrequency();
    }
    
    public void setLoop(boolean b) {
        isLoop = b;
        loopMessage = null;
    }

    public void putLoopMessage(String loop) {
        loopMessage = loop;
    }

    /**
     * @return the isLoop
     */
    public boolean isIsLoop() {
        return isLoop;
    }

    /**
     * AudioThread is responsible for pulling characters out of the send buffer
     * converting them to a waveform, and writing them to the audio output line.
     */
    class AudioThread extends Thread {

        private boolean kill;
        private final ByteBuffer buf;

        public AudioThread() {
            // we'll make our buffer the same size as our output line's buffer.
            // typically this is about one second's worth of samples, but could 
            // vary.
            buf = ByteBuffer.allocate(line.getBufferSize());
        }

        @Override
        public void run() {

            while (!kill) {

                //get next char to send.  If none, we'll transmit an IDLE...
                Character c = null;  // null converts to IDLE 
                synchronized (charLock) {
                    if (!txQueue.isEmpty()) {
                        if (isTx){
                            c = txQueue.poll();
                        }
                    } else if (isIsLoop()) {
                        putString(loopMessage);
                        if (isTx){
                            c = txQueue.poll();
                        }
                    }
                }

                // dds takes the char and converts it to baseband signal
                // normalized from -1.0 to 1.0 for intput to the mixer
                double[] baseBand;
                if (c == null) {
                    baseBand = dds.getSamples(null); // null converts to IDLE
                } else {
                    baseBand = dds.getSamples(c);
                }

                // The mixer places the baseband singal on the carrier provided 
                // by the LO.
                
                // we always mix even if we are going to throw it away
                // because it keeps everything in sync.
                double[] waveform = m.mix(baseBand);
                
                // if we are not transmitting, we'll mute the output by
                // sending zeros instead of the mixer output.
                if (!isTx) {
                    waveform = new double[waveform.length];
                }

                // plays it out the audio system
                play(waveform);
                
                // echo the char
                if (c != null) {
                    notifyListeners(c);
                }
            }
            line.stop();
            line.close();
        }

        private void play(double[] waveform) {
            buf.clear();
            for (int i = 0; i < waveform.length; i++) {
                buf.putShort((short) (Short.MAX_VALUE * waveform[i]));
            }
            //If the audio buffer is full, this will block until there is room
            line.write(buf.array(), 0, buf.position());
        }

        /**
         * Shuts off the output immediately.
         */
        private void kill() {
            kill = true;
        }

        private synchronized void notifyListeners(Character c) {
           for (ITxListener l : listeners) {
               l.TxCharacterSent(c);
           }
        }
    }
}
