/**
 * Copyright 2013-2023 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */

package com.jogamp.openal;

import com.jogamp.common.GlueGenVersion;

import com.jogamp.common.os.Platform;
import com.jogamp.common.util.VersionUtil;
import com.jogamp.common.util.JogampVersion;
import com.jogamp.openal.util.ALHelpers;

import java.util.jar.Manifest;

public class JoalVersion extends JogampVersion {
    protected static volatile JoalVersion jogampCommonVersionInfo;

    protected JoalVersion(final String packageName, final Manifest mf) {
        super(packageName, mf);
    }

    public static JoalVersion getInstance() {
        if(null == jogampCommonVersionInfo) { // volatile: ok
            synchronized(JoalVersion.class) {
                if( null == jogampCommonVersionInfo ) {
                    final String packageName = "com.jogamp.openal";
                    final Manifest mf = VersionUtil.getManifest(JoalVersion.class.getClassLoader(), packageName);
                    jogampCommonVersionInfo = new JoalVersion(packageName, mf);
                }
            }
        }
        return jogampCommonVersionInfo;
    }

    public StringBuilder getBriefOSALBuildInfo(StringBuilder sb) {
        if(null==sb) {
            sb = new StringBuilder();
        }
        sb.append("OS: ").append(Platform.getOSName()).append(", version ").append(Platform.getOSVersion()).append(", arch ").append(Platform.getArchName());
        sb.append(Platform.getNewline());
        sb.append("JOAL GIT sha1 ").append(getImplementationCommit());
        sb.append(Platform.getNewline());
        return sb;
    }

    /**
     * Return {@link JogampVersion} package information and AL informal strings.
     * <p>
     * The given {@link ALC} is being used and {@Link ALCdevice} and {@link ALCcontext} are allocated,
     * made current and finally being released.
     * </p>
     * @param alc static {@link ALC} instance
     * @param sb optional StringBuffer to be reused
     */
    public StringBuilder toString(final ALC alc, StringBuilder sb) {
        sb = super.toString(sb).append(Platform.getNewline());
        getALStrings(alc, sb);
        return sb;
    }

    /**
     * Return {@link JogampVersion} package information and AL informal strings.
     * <p>
     * The given {@link ALC} is being used and {@Link ALCdevice} and {@link ALCcontext} are allocated,
     * made current and finally being released.
     * </p>
     * @param alc static {@link ALC} instance
     */
    public String toString(final ALC alc) {
        return toString(alc, null).toString();
    }

    /**
     * Return AL informal strings.
     * <p>
     * The given {@link ALC} is being used and {@Link ALCdevice} and {@link ALCcontext} are allocated,
     * made current and finally being released.
     * </p>
     * @param alc static {@link ALC} instance
     */
    public static StringBuilder getALStrings(final ALC alc, StringBuilder sb) {
        if( null == sb ) {
            sb = new StringBuilder();
        }
        if( null == alc ) {
            sb.append("ALC null");
            return sb;
        }
        final ALCcontext initialContext = alc.alcGetCurrentContext();

        final ALCcontext context;
        final ALCdevice device;
        if( null == initialContext) {
            device = alc.alcOpenDevice(null);
            context = alc.alcCreateContext(device, null);
            alc.alcMakeContextCurrent(context);
        } else {
            context = initialContext;
            device = alc.alcGetContextsDevice(initialContext);
        }
        final AL al = ALFactory.getAL(); // valid after makeContextCurrent(..)
        final ALVersion alv = new ALVersion(al);

        alv.toString(true, sb);
        sb.append("AL_EXTENSIONS  ").append(al.alGetString(ALConstants.AL_EXTENSIONS));
        sb.append(Platform.getNewline());
        final boolean enumerationExtIsPresent = alc.aclEnumerationExtIsPresent();
        final boolean enumerateAllExtIsPresent = alc.aclEnumerateAllExtIsPresent();
        final String enumExtAvailInfo = "(enumExt[def "+enumerationExtIsPresent+", all "+enumerateAllExtIsPresent+"])";
        {
            final int[] iversion = { 0, 0 };
            alc.alcGetIntegerv(device, ALCConstants.ALC_MAJOR_VERSION, 1, iversion, 0);
            alc.alcGetIntegerv(device, ALCConstants.ALC_MINOR_VERSION, 1, iversion, 1);
            sb.append("ALC_VERSION     ").append(iversion[0]).append(".").append(iversion[1]);
            sb.append(Platform.getNewline());
            if (!enumerationExtIsPresent && !enumerateAllExtIsPresent) {
                sb.append("ALC_DEF_OUTPUT Unknown ").append(enumExtAvailInfo);
                sb.append(Platform.getNewline());
            } else {
                if (enumerationExtIsPresent) {
                    sb.append("ALC_DEF_OUTPUT (With " + ALHelpers.ALC_ENUMERATION_EXT + ") ")
                            .append(alc.alcGetString(device, ALCConstants.ALC_DEFAULT_DEVICE_SPECIFIER));
                    sb.append(Platform.getNewline());
                }
                if (enumerateAllExtIsPresent) {
                    sb.append("ALC_DEF_OUTPUT (With " + ALHelpers.ALC_ENUMERATE_ALL_EXT + ") ")
                            .append(alc.alcGetString(device, ALCConstants.ALC_DEFAULT_ALL_DEVICES_SPECIFIER));
                    sb.append(Platform.getNewline());
                }
            }
            if (enumerationExtIsPresent) {
                sb.append("ALC_DEF_CAPTURE ").append(alc.alcGetString(device, ALCConstants.ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER));
            } else {
                sb.append("ALC_DEF_CAPTURE Unknown ").append(enumExtAvailInfo);
            }
            sb.append(Platform.getNewline());
        }

        if( null == initialContext ) {
            alc.alcMakeContextCurrent(null);
            alc.alcDestroyContext(context);
            alc.alcCloseDevice(device);
        }

        devicesToString(sb, alc);
        return sb;
    }

    private static boolean checkALCError(final ALC alc, final ALCdevice device, final String msg)
    {
      final int error = alc.alcGetError(device);
      if (error != ALCConstants.ALC_NO_ERROR) {
        System.err.printf("ALC Error 0x%x occurred: '%s' while '%s'%n", error, alc.alcGetString(device, error), msg);
        return true;
      }
      return false;
    }

    public static void deviceToString(final StringBuilder sb, final ALC alc, final String devName, final boolean isInput, final String defOutDeviceName, final String defInDeviceName) {
        if( isInput ) {
            final boolean isDefault = devName.equals(defInDeviceName);
            sb.append("    "+devName+", input, default "+isDefault+System.lineSeparator());
        } else {
            final boolean isDefault = devName.equals(defOutDeviceName);
            final String defStr = isDefault ? "default " : "";
            final String inOutStr = "output";
            final int mixerFrequency, mixerRefresh, monoSourceCount, stereoSourceCount;
            final int[] val = { 0 };
            final ALCcontext initialContext = alc.alcGetCurrentContext();
            final ALCdevice initialDevice = initialContext != null ? alc.alcGetContextsDevice(initialContext) : null;
            final ALCdevice d = alc.alcOpenDevice(devName);
            if( null == d ) {
                System.err.println("Error: Failed to open "+defStr+inOutStr+" device "+devName);
                return;
            }
            final ALCcontext c = alc.alcCreateContext(d, null);
            if( null == c ) {
                System.err.println("Error: Failed to create context for "+defStr+inOutStr+" device "+devName);
                alc.alcCloseDevice(d);
                return;
            }
            if( !alc.alcMakeContextCurrent(c) ) {
                System.err.println("Error: Failed to make context current for "+defStr+inOutStr+" device "+devName);
                checkALCError(alc, d, "alcMakeContextCurrent");
                alc.alcDestroyContext(c);
                alc.alcCloseDevice(d);
                return;
            }

            {
                val[0] = 0;
                alc.alcGetIntegerv(d, ALCConstants.ALC_FREQUENCY, 1, val, 0);
                if( checkALCError(alc, d, "read ALC_FREQUENCY") ) {
                    mixerFrequency = -1;
                } else {
                    mixerFrequency = val[0];
                }
            }
            {
                val[0] = 0;
                alc.alcGetIntegerv(d, ALCConstants.ALC_REFRESH, 1, val, 0);
                if( checkALCError(alc, d, "read ALC_REFRESH") ) {
                    mixerRefresh = -1;
                } else {
                    mixerRefresh = val[0];
                }
            }
            {
                val[0] = 0;
                alc.alcGetIntegerv(d, ALCConstants.ALC_MONO_SOURCES, 1, val, 0);
                if( checkALCError(alc, d, "read ALC_MONO_SOURCES") ) {
                    monoSourceCount = -1;
                } else {
                    monoSourceCount = val[0];
                }
            }
            {
                val[0] = 0;
                alc.alcGetIntegerv(d, ALCConstants.ALC_STEREO_SOURCES, 1, val, 0);
                if( checkALCError(alc, d, "read ALC_STEREO_SOURCES") ) {
                    stereoSourceCount = -1;
                } else {
                    stereoSourceCount = val[0];
                }
            }
            sb.append("    "+devName+", "+inOutStr+", default "+isDefault+", mixer[freq "+mixerFrequency+", refresh "+mixerRefresh+
                    " (min latency "+(1000f/mixerRefresh)+" ms)], sources[mono "+monoSourceCount+", stereo "+stereoSourceCount+"]"+
                    System.lineSeparator());

            if( null != initialContext ) {
                alc.alcMakeContextCurrent(initialContext);
                if( initialContext.getDirectBufferAddress() != c.getDirectBufferAddress() ) {
                    alc.alcDestroyContext(c);
                }
            } else {
                alc.alcMakeContextCurrent(null);
                alc.alcDestroyContext(c);
            }
            if( initialDevice == null || initialDevice.getDirectBufferAddress() != d.getDirectBufferAddress() ) {
                alc.alcCloseDevice(d);
            }
        }
    }

    public static void devicesToString(final StringBuilder sb, final ALC alc) {
        final boolean enumerationExtIsPresent = alc.aclEnumerationExtIsPresent();
        final boolean enumerateAllExtIsPresent = alc.aclEnumerateAllExtIsPresent();
        final String enumExtAvailInfo = "(enumExt[def "+enumerationExtIsPresent+", all "+enumerateAllExtIsPresent+"])";

        if (!enumerationExtIsPresent && !enumerateAllExtIsPresent) {
            sb.append("No output devices infos available ").append(enumExtAvailInfo);
        } else {
            if (enumerateAllExtIsPresent) {
                final String defOutAllDeviceName = alc.alcGetString(null, ALCConstants.ALC_DEFAULT_ALL_DEVICES_SPECIFIER);
                sb.append("Output devices (With " + ALHelpers.ALC_ENUMERATE_ALL_EXT + "):" + System.lineSeparator());
                {
                    final String[] outDevices = alc.alcGetAllDeviceSpecifiers();
                    if (null != outDevices) {
                        for (final String devName : outDevices) {
                            deviceToString(sb, alc, devName, false, defOutAllDeviceName, null);
                        }
                    }
                }
            }
            if (enumerationExtIsPresent) {
                final String defOutDeviceName = alc.alcGetString(null, ALCConstants.ALC_DEFAULT_DEVICE_SPECIFIER);
                sb.append("Output devices (With " + ALHelpers.ALC_ENUMERATION_EXT + "):" + System.lineSeparator());
                {
                    final String[] outDevices = alc.alcGetDeviceSpecifiers();
                    if (null != outDevices) {
                        for (final String devName : outDevices) {
                            deviceToString(sb, alc, devName, false, defOutDeviceName, null);
                        }
                    }
                }
            }
        }
        if (!enumerationExtIsPresent) {
            sb.append("No capture devices infos available ").append(enumExtAvailInfo);
        } else {
            final String defInDeviceName = alc.alcGetString(null, ALCConstants.ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER);
            sb.append("Capture devices:" + System.lineSeparator());
            {
                final String[] inDevices = alc.alcGetCaptureDeviceSpecifiers();
                if (null != inDevices) {
                    for (final String devName : inDevices) {
                        deviceToString(sb, alc, devName, true, null, defInDeviceName);
                    }
                }
            }
        }
    }

    public static void main(final String args[]) {
        System.err.println(VersionUtil.getPlatformInfo());
        System.err.println(GlueGenVersion.getInstance());
        // System.err.println(NativeWindowVersion.getInstance());
        // System.err.println(JoalVersion.getInstance());
        System.err.println(JoalVersion.getInstance().toString(ALFactory.getALC()));

    }
}