/** 
* Copyright 2024 Brendan Robert
*
* 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 jace.apple2e;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;

import jace.Emulator;
import jace.LawlessLegends;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import jace.core.Device;
import jace.core.RAMEvent;
import jace.core.RAMListener;
import jace.core.SoundMixer;
import jace.core.SoundMixer.SoundBuffer;
import jace.core.SoundMixer.SoundError;
import jace.core.TimedDevice;
import jace.core.Utility;
import javafx.stage.FileChooser;

/**
 * Apple // Speaker Emulation Created on May 9, 2007, 9:55 PM
 *
 * @author Brendan Robert (BLuRry) brendan.robert@gmail.com
 */
public class Speaker extends Device {

    static boolean fileOutputActive = false;
    static OutputStream out;
    
    @ConfigurableField(category = "sound", name = "1mhz timing", description = "Force speaker output to 1mhz?")
    public static boolean force1mhz = true;

    @ConfigurableField(category = "sound", name = "Show sound", description = "Use black color value to show sound output")
    public static boolean showSound = false;

    @InvokableAction(category = "sound", name = "Record sound", description="Toggles recording (saving) sound output to a file", defaultKeyMapping = "ctrl+shift+w")
    public static void toggleFileOutput() {
        if (fileOutputActive) {
            try {
                out.close();
            } catch (IOException ex) {
                Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
            }
            out = null;
            fileOutputActive = false;
        } else {
            FileChooser fileChooser = new FileChooser();
            File f = fileChooser.showSaveDialog(LawlessLegends.getApplication().primaryStage);
            if (f == null) {
                return;
            }
            try {
                out = new FileOutputStream(f);
                fileOutputActive = true;
            } catch (FileNotFoundException ex) {
                Logger.getLogger(Speaker.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
    
    /**
     * Counter tracks the number of cycles between sampling
     */
    private double counter = 0;
    /**
     * Level is the number of cycles the speaker has been on
     */
    private int level = 0;
    /**
     * Idle cycles counts the number of cycles the speaker has not been changed
     * (used to deactivate sound when not in use)
     */
    private int idleCycles = 0;
    /**
     * Playback volume (should be < 1423)
     */
    @ConfigurableField(name = "Speaker Volume", shortName = "vol", description = "Should be under 1400")
    public static int VOLUME = 400;
    private int currentVolume = 0;
    private int fadeOffAmount = 1;
    
    /**
     * Volume scaling factor (0.0 to 1.0)
     */
    private double volumeScale = 0.5;
    
    /**
     * Set the speaker volume scaling factor
     * @param scale Volume scale between 0.0 and 1.0
     */
    public void setVolumeScale(double scale) {
        volumeScale = Math.max(0.0, Math.min(1.0, scale));
    }
    
    /**
     * Get the current volume scaling factor
     * @return Volume scale between 0.0 and 1.0
     */
    public double getVolumeScale() {
        return volumeScale;
    }
    /**
     * Manifestation of the apple speaker softswitch
     */
    private boolean speakerBit = false;
    private static double TICKS_PER_SAMPLE = ((double) TimedDevice.NTSC_1MHZ) / SoundMixer.RATE;
    private RAMListener listener = null;
    private SoundBuffer buffer = null;

    /**
     * Number of idle cycles until speaker playback is deactivated
     */
    @ConfigurableField(name = "Idle cycles before sleep", shortName = "idle")
    // public static int MAX_IDLE_CYCLES = (int) (SoundMixer.BUFFER_SIZE * TICKS_PER_SAMPLE * 2);
    public static int MAX_IDLE_CYCLES = (int) TimedDevice.NTSC_1MHZ / 4;

    /**
     * Suspend playback of sound
     *
     * @return
     */
    @Override
    public boolean suspend() {
        boolean result = super.suspend();
        speakerBit = false;
        if (buffer != null) {
            try {
                buffer.shutdown();
            } catch (InterruptedException | ExecutionException | SoundError e) {
                // Ignore
            } finally {
                buffer = null;
            }
        }
        Emulator.withComputer(c->c.getMotherboard().cancelSpeedRequest(this));

        return result;
    }

    /**
     * Start or resume playback of sound
     */
    @Override
    public void resume() {
        if (Utility.isHeadlessMode()) {
            return;
        }
        if (buffer == null || !buffer.isAlive()) {
            try {
                buffer = SoundMixer.createBuffer(false);
            } catch (InterruptedException | ExecutionException | SoundError e) {
                e.printStackTrace();
                detach();
                return;
            }
        }
        if (buffer != null) {
            counter = 0;
            idleCycles = 0;
            level = 0;
        } else {
            Logger.getLogger(getClass().getName()).severe("Unable to get audio buffer for speaker!");
            detach();
            return;
        }

        if (force1mhz) {
            TICKS_PER_SAMPLE = ((double) TimedDevice.NTSC_1MHZ) / SoundMixer.RATE;
        } else {
            TICKS_PER_SAMPLE = Emulator.withComputer(c-> ((double) c.getMotherboard().getSpeedInHz()) / SoundMixer.RATE, 0.0);
        }
        super.resume();
    }

    /**
     * Reset idle counter whenever sound playback occurs
     */
    public void resetIdle() {
        currentVolume = VOLUME;
        idleCycles = 0;
        if (!isRunning()) {
            resume();
        }
    }

    /**
     * Motherboard cycle tick Every 23 ticks a sample will be added to the
     * buffer If the buffer is full, this will block until there is room in the
     * buffer, thus keeping the emulation in sync with the sound
     */
    @Override
    public void tick() {
        if (speakerBit) {
            level++;
            if (showSound) {
                VideoNTSC.CHANGE_BLACK_COLOR(40, 20, 20);
            }
        } else if (showSound) {
            VideoNTSC.CHANGE_BLACK_COLOR(20,20,40);
        }
        if (idleCycles++ >= MAX_IDLE_CYCLES && (currentVolume <= 0 || !speakerBit)) {
            suspend();
            if (showSound) {
                VideoNTSC.CHANGE_BLACK_COLOR(0,0,0);
            }
        }
        counter += 1.0d;
        if (counter >= TICKS_PER_SAMPLE) {
            if (idleCycles >= MAX_IDLE_CYCLES) {
                currentVolume -= fadeOffAmount;
            }
            playSample((int)(level * currentVolume * volumeScale));
            // Emulator.withComputer(c->c.getMotherboard().requestSpeed(this));

            // Set level back to 0
            level = 0;
            // Set counter to 0
            counter -= TICKS_PER_SAMPLE;
        }
    }

    private void toggleSpeaker(RAMEvent e) {
        // if (e.getType() == RAMEvent.TYPE.WRITE) {
        //     level += 2;
        // }
        speakerBit = !speakerBit;
        resetIdle();
    }
    
    private void playSample(int sample) {
        try {
            if (buffer == null || !buffer.isAlive()) {
                // Logger.getLogger(getClass().getName()).severe("Audio buffer not initalized properly!");
                buffer = SoundMixer.createBuffer(false);
                if (buffer == null) {
                    System.err.println("Unable to create emergency audio buffer, detaching speaker");
                    detach();
                    return;
                }
            }
            buffer.playSample((short) sample);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } catch (SoundError e) {
            System.err.println("Sound error, detaching speaker: " + e.getMessage());
            e.printStackTrace();
            detach();
            buffer = null;
        }

        if (fileOutputActive) {
            byte[] bytes = new byte[2];
            bytes[0] = (byte) (sample & 0x0ff);
            bytes[1] = (byte) ((sample >> 8) & 0x0ff);

            try {
                out.write(bytes, 0, 2);
            } catch (IOException ex) {
                Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Error recording sound", ex);
                toggleFileOutput();
            }
        }
        
    }

    /**
     * Add a memory event listener for C03x for capturing speaker events
     */
    private void configureListener() {
        listener = Emulator.withMemory(m->m.observe("Speaker", RAMEvent.TYPE.ANY, 0x0c030, 0x0c03f, this::toggleSpeaker), null);
    }

    private void removeListener() {
        Emulator.withMemory(m->m.removeListener(listener));
    }

    /**
     * Returns "Speaker"
     *
     * @return "Speaker"
     */
    @Override
    protected String getDeviceName() {
        return "Speaker";
    }

    @Override
    public String getShortName() {
        return "spk";
    }

    @Override
    public final void reconfigure() {
    }

    @Override
    public void attach() {
        configureListener();
    }

    @Override
    public void detach() {
        removeListener();
        super.detach();
    }
}
