// *************************************************************************
//   Copyright (C) 2017 by Paul Lutus                                      *
//   lutusp@arachnoid.com                                                  *
//                                                                         *
//   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 2 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, write to the                         *
//   Free Software Foundation, Inc.,                                       *
//   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
// *************************************************************************
package morse_sender;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.SourceDataLine;

/**
 *
 * @author lutusp
 */
final public class MorseSender {

    // this value controls the code waveform envelope (ascent and descent)
    double envelopeConstant = .005;

    // http://www.nu-ware.com/NuCode%20Help/index.html?morse_code_structure_and_timing_.htm
    // http://www.arrl.org/code-practice-files (for samples of good sending)

    /* a dot's duration is a dot-time tone followed by a dot-time of silence
     * therefore the theoretical time duration in seconds for one dot is 1.2 / wpm
     * this differs slightly from the the ARRL morse code practice recordings 
     */
    double dotConstant = 1.2;
    double dot_time_between_dots_dashes = 1;
    double dot_time_between_characters = 2; // result = 3
    double dot_time_between_words = 4; // result = 7

    // default values before command-line arguments
    double sampleRate = 44100;
    double freqHz = 750;
    double morseWPM = 13;
    double dotDuration;
    double level = 0.5;
    boolean streamInput = false;
    String filePath = null;
    AudioFormat af;
    SourceDataLine sdl;
    HashMap<String, String> charHash;
    HashMap<String, String> transHash;

    // translate Unicode and other
    // characters into common ones
    final String[] transTable = {
        "“", "\"", // smart quote to quote
        "”", "\"", // smart quote to quote
        "’", "'", // smart apostrophe to apostrophe
        "‘", "'", // Unicode smart apostrophe to apostrophe
        "`", "'", // accent to apostrophe
        "—", "-", // Unicode dash to dash
    };

    final String[] morseTable = {
        "!", "-.-.--",
        "\"", ".-..-.",
        "$", "...-..-",
        "'", ".----.",
        "/", "-..-.",
        "(", "-.--.",
        ")", "-.--.-",
        "[", "-.--.",
        "]", "-.--.-",
        "+", ".-.-.",
        ",", "--..--",
        "-", "-....-",
        ".", ".-.-.-",
        "_", "..--.-",
        "/", "-..-.",
        "0", "-----",
        "1", ".----",
        "2", "..---",
        "3", "...--",
        "4", "....-",
        "5", ".....",
        "6", "-....",
        "7", "--...",
        "8", "---..",
        "9", "----.",
        ":", "---...",
        ";", "-.-.-.",
        "=", "-...-",
        "@", ".--.-.",
        "?", "..--..",
        "a", ".-",
        "b", "-...",
        "c", "-.-.",
        "d", "-..",
        "e", ".",
        "f", "..-.",
        "g", "--.",
        "h", "....",
        "i", "..",
        "j", ".---",
        "k", "-.-",
        "l", ".-..",
        "m", "--",
        "n", "-.",
        "o", "---",
        "p", ".--.",
        "q", "--.-",
        "r", ".-.",
        "s", "...",
        "t", "-",
        "u", "..-",
        "v", "...-",
        "w", ".--",
        "x", "-..-",
        "y", "-.--",
        "z", "--..",};

    public MorseSender(double freqHz, double morseWPM, double level) {
        this.freqHz = freqHz;
        this.morseWPM = morseWPM;
        this.level = level;
        init();
    }

    public MorseSender(String[] args) {
        process(args);
    }

    private void init() {
        dotDuration = dotConstant / morseWPM;
        charHash = new HashMap<>();
        int i = 0;
        while (i < morseTable.length) {
            charHash.put(morseTable[i], morseTable[i + 1]);
            i += 2;
        }
        transHash = new HashMap<>();
        i = 0;
        while (i < transTable.length) {
            transHash.put(transTable[i], transTable[i + 1]);
            i += 2;
        }
    }

    private void p(String s) {
        System.out.println(s);
    }

    private double envelope(double a, double b, double t, double tc) {
        return ((b - t) * (-a + t)) / ((b - t + tc) * (-a + t + tc));
    }

    private void sendElement(double durationSec, boolean sound) {
        try {
            int bsize = (int) (sampleRate * durationSec) * 2;
            byte[] buf = new byte[bsize];
            double step = 2.0 * Math.PI * freqHz / sampleRate;
            double ec = envelopeConstant * sampleRate;
            double angle = 0;
            int i = 0;
            while (i < bsize) {

                int n = 0;
                if (sound) {
                    n = (int) (Math.sin(angle) * level * 32767 * envelope(0, bsize, i, ec));
                }
                buf[i++] = (byte) (n % 256);
                buf[i++] = (byte) (n / 256);
                angle += step;
            }
            sdl.write(buf, 0, buf.length);
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }

    private void sendString(String s) {
        for (char sc : s.toCharArray()) {
            System.out.print(sc);
            String slc = Character.toString(sc).toLowerCase();
            /* durations for:
             *  dot-time between dots/dashes : 1
             *  added dot-time between characters : 4, total 5
             * added dot-time between words : 4, total 9
             */
            if (slc.equals(" ")) {
                // between words
                sendElement(dotDuration * dot_time_between_words, false);
            } else {
                if (transHash.containsKey(slc)) {
                    slc = transHash.get(slc);
                }
                if (charHash.containsKey(slc)) {
                    String ms = charHash.get(slc);
                    for (char c : ms.toCharArray()) {
                        // . = 1 dot duration, - = 3 dot durations
                        double dur = dotDuration * ((c == '.') ? 1 : 3);
                        sendElement(dur, true);
                        // pause between dots and dashes of 1 dot duration
                        sendElement(dotDuration * dot_time_between_dots_dashes, false);
                    }
                    // between characters
                    sendElement(dotDuration * dot_time_between_characters, false);
                }
            }
        }
    }

    private void openAudio() {
        try {
            af = new AudioFormat((float) sampleRate, 16, 1, true, false);
            sdl = AudioSystem.getSourceDataLine(af);
            sdl.open(af);
            sdl.start();
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }

    private void closeAudio() {
        try {
            sdl.drain();
            sdl.close();
        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }

    public void process(String args[]) {
        if (args.length == 0) {
            System.out.println(String.format("Usage: [-s code speed wpm default %.0f]", morseWPM));
            System.out.println(String.format("       [-f freq default %.0f]", freqHz));
            System.out.println("       [-i (text file name) for input instead of command line]");
            System.out.println(String.format("       [-r sample rate default %.0f]", sampleRate));
            System.out.println(String.format("       [-v volume level (0 <= v <= 1), default %.1f]", level));
            System.out.println("       [-p read data from stdin]");
            System.out.println(String.format("       [-c delay between characters, default %.0f]", this.dot_time_between_characters));
            System.out.println(String.format("       [-w delay between words, default %.0f]", dot_time_between_words));

            System.out.println("       string to translate into Morse code.");
        } else {
            ArrayList<String> chars = new ArrayList<>();
            for (int i = 0; i < args.length; i++) {
                String s = args[i];
                if (s.charAt(0) == '-') {
                    switch (s) {
                        case "-s":
                            morseWPM = Double.parseDouble(args[i + 1]);
                            i += 1;
                            break;
                        case "-f":
                            freqHz = Double.parseDouble(args[i + 1]);
                            i += 1;
                            break;
                        case "-v":
                            level = Double.parseDouble(args[i + 1]);
                            i += 1;
                            break;
                        case "-r":
                            sampleRate = Integer.parseInt(args[i + 1]);
                            i += 1;
                            break;
                        case "-p":
                            streamInput = true;
                            break;
                        case "-i":
                            filePath = args[i + 1];
                            i += 1;
                            break;
                        case "-w":
                            dot_time_between_words = Double.parseDouble(args[i + 1]);
                            i += 1;
                            break;
                        case "-c":
                            dot_time_between_characters = Double.parseDouble(args[i + 1]);
                            i += 1;
                            break;
                        default:
                            System.err.println("Error: don't know option \"" + s + "\".");
                            break;
                    }
                } else {
                    chars.add(s);
                }
            }
            init();
            try {
                openAudio();
                if (streamInput) {
                    String s;
                    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
                    while ((s = in.readLine()) != null) {
                        if (s.length() > 0) {
                            sendString(s);
                        }
                    }
                } else if (filePath != null) {
                    String data = new String(Files.readAllBytes(Paths.get(filePath)));
                    data = data.trim();
                    sendString(data);

                } else {
                    String sa = String.join(" ", chars);
                    sendString(sa);
                }
                closeAudio();
            } catch (Exception e) {
                e.printStackTrace(System.out);
            }
            System.out.println();
        }
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        MorseSender ms = new MorseSender(args);
    }
}
