/*
    Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
    и других спецификаций для функционирования компактных приложений на языке
    Java (мидлетов) в среде программного обеспечения Малик Эмулятор.

    Copyright © 2016–2017, 2019–2022 Малик Разработчик

    Это свободная программа: вы можете перераспространять ее и/или изменять
    ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она будет полезной,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <https://www.gnu.org/licenses/>.
*/

package com.nokia.mid.sound;

import java.io.*;
import javax.microedition.media.*;
import javax.microedition.media.control.*;

public class Sound extends Object
{
    public static final int FORMAT_TONE = 1; /* устаревший формат */
    public static final int FORMAT_WAV = 5;
    public static final int SOUND_PLAYING = 0;
    public static final int SOUND_STOPPED = 1;
    public static final int SOUND_UNINITIALIZED = 3;

    private static boolean formatToneNotImplementedNotified;
    private static final byte[] EMPTY_MIDI;

    static {
        EMPTY_MIDI = new byte[] {
            (byte) 0x4d, (byte) 0x54, (byte) 0x68, (byte) 0x64,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x06,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x64,
            (byte) 0x4d, (byte) 0x54, (byte) 0x72, (byte) 0x6b,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0c,
            (byte) 0x00, (byte) 0xff, (byte) 0x51, (byte) 0x03, (byte) 0x01, (byte) 0x86, (byte) 0xa0,
            (byte) 0x87, (byte) 0x68, (byte) 0xff, (byte) 0x2f, (byte) 0x00
        };
    }

    public static int getConcurrentSoundCount(int format) {
        switch(format)
        {
        default:
            throw new IllegalArgumentException("Sound.getConcurrentSoundCount: аргумент format имеет недопустимое значение.");
        case FORMAT_TONE:
        case FORMAT_WAV:
            return 255;
        }
    }

    public static int[] getSupportedFormats() {
        return new int[] { FORMAT_TONE, FORMAT_WAV };
    }

    private int state;
    private Player implement;
    private VolumeControl volume;
    private SoundListener listener;
    private final PlayerListener gate;

    public Sound(int frequency, long duration) {
        this.gate = createGate();
        recreate(frequency, duration);
    }

    public Sound(byte[] data, int format) {
        this.gate = createGate();
        recreate(data, format);
    }

    public void init(int frequency, long duration) {
        recreate(frequency, duration);
    }

    public void init(byte[] data, int format) {
        recreate(data, format);
    }

    public void release() {
        Player implement;
        if((implement = this.implement) != null)
        {
            this.state = SOUND_UNINITIALIZED;
            this.implement = null;
            this.volume = null;
            implement.close();
        }
    }

    public void resume() {
        Player implement;
        if((implement = this.implement) != null)
        {
            this.state = SOUND_PLAYING;
            try
            {
                implement.start();
            }
            catch(MediaException e)
            {
                e.printRealStackTrace();
            }
        }
    }

    public void play(int loopCount) {
        Player implement;
        if(loopCount < 0)
        {
            throw new IllegalArgumentException("Sound.play: аргумент loopCount не может быть отрицательным.");
        }
        if(loopCount >= 255) return;
        if(loopCount == 0) loopCount = -1;
        if((implement = this.implement) != null)
        {
            this.state = SOUND_PLAYING;
            try
            {
                implement.stop();
                implement.setLoopCount(loopCount);
                implement.start();
            }
            catch(MediaException e)
            {
                e.printRealStackTrace();
            }
        }
    }

    public void stop() {
        Player implement;
        if((implement = this.implement) != null)
        {
            this.state = SOUND_STOPPED;
            try
            {
                implement.stop();
            }
            catch(MediaException e)
            {
                e.printRealStackTrace();
            }
        }
    }

    public void setSoundListener(SoundListener listener) {
        this.listener = listener;
    }

    public void setGain(int volumeLevel) {
        VolumeControl volume;
        if(volumeLevel < 0) volumeLevel = 0;
        if(volumeLevel > 255) volumeLevel = 255;
        if(volumeLevel > 0) volumeLevel = ((volumeLevel - 1) * 99 / 254) + 1;
        if((volume = this.volume) != null)
        {
            try
            {
                volume.setLevel(volumeLevel);
            }
            catch(Exception e)
            {
                e.printRealStackTrace();
            }
        }
    }

    public int getGain() {
        int result;
        VolumeControl volume;
        if((volume = this.volume) == null) return -1;
        if((result = volume.getLevel()) > 0) result = ((result - 1) * 254 / 99) + 1;
        return result;
    }

    public int getState() {
        return state;
    }

    final void notify(int state) {
        SoundListener listener;
        this.state = state;
        if((listener = this.listener) != null) listener.soundStateChanged(this, state);
    }

    private strictfp void recreate(int frequency, long duration) {
        int time;
        int note;
        byte[] midi;
        Player implement;
        if(frequency < 0)
        {
            throw new IllegalArgumentException("Sound.init: аргумент frequency не может быть отрицательным.");
        }
        if(duration <= 0L)
        {
            throw new IllegalArgumentException("Sound.init: аргумент duration может быть только положительным.");
        }
        time = duration > 10000L ? 10000 : (int) duration;
        if((note = frequency <= 0 ? 0 : (int) Math.round(12.d * Math.log2((double) frequency) + (-36.3763165622959152d))) < 0x00)
        {
            note = 0x00;
        }
        else if(note > 0x7f)
        {
            note = 0x7f;
        }
        midi = new byte[] {
            (byte) 0x4d, (byte) 0x54, (byte) 0x68, (byte) 0x64,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x06,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x64,
            (byte) 0x4d, (byte) 0x54, (byte) 0x72, (byte) 0x6b,
            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x14,
            (byte) 0x00, (byte) 0xff, (byte) 0x51, (byte) 0x03, (byte) 0x01, (byte) 0x86, (byte) 0xa0,
            (byte) 0x00, (byte) 0x90, (byte) note, (byte) 0x7f,
            (byte) (0x80 | time >> 7), (byte) (time & 0x7f), (byte) note, (byte) 0x00,
            (byte) (0x80 | time >> 7), (byte) (time & 0x7f), (byte) 0xff, (byte) 0x2f, (byte) 0x00
        };
        if((implement = this.implement) != null) implement.close();
        try
        {
            init(Manager.createPlayer(new ByteArrayInputStream(midi), "audio/midi"));
        }
        catch(Exception e)
        {
            e.printRealStackTrace();
        }
    }

    private void recreate(byte[] data, int format) {
        Player implement;
        if(data == null)
        {
            throw new NullPointerException("Sound.init: аргумент data равен нулевой ссылке.");
        }
        switch(format)
        {
        case FORMAT_TONE:
            if(!formatToneNotImplementedNotified)
            {
                formatToneNotImplementedNotified = true;
                System.out.println("/!\\ Sound.init: формат TONE устарел и не поддерживается этой реализацией.");
            }
            if((implement = this.implement) != null) implement.close();
            try
            {
                init(Manager.createPlayer(new ByteArrayInputStream(EMPTY_MIDI), "audio/midi"));
            }
            catch(Exception e)
            {
                throw new IllegalArgumentException(e.getMessage());
            }
            break;
        case FORMAT_WAV:
            if((implement = this.implement) != null) implement.close();
            try
            {
                init(Manager.createPlayer(new ByteArrayInputStream(data), "audio/x-wav"));
            }
            catch(Exception e)
            {
                throw new IllegalArgumentException(e.getMessage());
            }
            break;
        default:
            throw new IllegalArgumentException("Sound.init: аргумент format имеет недопустимое значение.");
        }
    }

    private void init(Player implement) throws MediaException {
        VolumeControl volume;
        implement.addPlayerListener(gate);
        implement.prefetch();
        volume = (VolumeControl) implement.getControl("VolumeControl");
        this.state = SOUND_STOPPED;
        this.implement = implement;
        this.volume = volume;
    }

    private PlayerListener createGate() {
        return new PlayerListener() {
            public void playerUpdate(Player player, String event, Object data) {
                Sound parent = Sound.this;
                if(CLOSED.equals(event))
                {
                    parent.notify(Sound.SOUND_UNINITIALIZED);
                    return;
                }
                if(STARTED.equals(event))
                {
                    parent.notify(Sound.SOUND_PLAYING);
                    return;
                }
                if(STOPPED.equals(event) || END_OF_MEDIA.equals(event)) parent.notify(Sound.SOUND_STOPPED);
            }
        };
    }
}
