/*
 * SpectrumTimeAnalyzer.
 *
 * JavaZOOM : jlgui@javazoom.net
 *            http://www.javazoom.net
 *
 *-----------------------------------------------------------------------
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library 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 Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */
package javazoom.jlgui.player.amp.visual.ui;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import javax.sound.sampled.SourceDataLine;
import javax.swing.JPanel;
import javazoom.jlgui.player.amp.skin.AbsoluteConstraints;
import kj.dsp.KJDigitalSignalProcessingAudioDataConsumer;
import kj.dsp.KJDigitalSignalProcessor;
import kj.dsp.KJFFT;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SpectrumTimeAnalyzer extends JPanel implements KJDigitalSignalProcessor
{
    private static Log log = LogFactory.getLog(SpectrumTimeAnalyzer.class);
    public static final int DISPLAY_MODE_SCOPE = 0;
    public static final int DISPLAY_MODE_SPECTRUM_ANALYSER = 1;
    public static final int DISPLAY_MODE_OFF = 2;
    public static final int DEFAULT_WIDTH = 256;
    public static final int DEFAULT_HEIGHT = 128;
    public static final int DEFAULT_FPS = 50;
    public static final int DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE = 512;
    public static final int DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT = 19;
    public static final float DEFAULT_SPECTRUM_ANALYSER_DECAY = 0.05f;
    public static final int DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY = 20;
    public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO = 0.4f;
    public static final float DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE = 0.1f;
    public static final float MIN_SPECTRUM_ANALYSER_DECAY = 0.02f;
    public static final float MAX_SPECTRUM_ANALYSER_DECAY = 0.08f;
    public static final Color DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 128);
    public static final Color DEFAULT_SCOPE_COLOR = new Color(255, 192, 0);
    public static final float DEFAULT_VU_METER_DECAY = 0.02f;
    private Image bi;
    private int displayMode = DISPLAY_MODE_SCOPE;
    private Color scopeColor = DEFAULT_SCOPE_COLOR;
    private Color[] spectrumAnalyserColors = getDefaultSpectrumAnalyserColors();
    private KJDigitalSignalProcessingAudioDataConsumer dsp = null;
    private boolean dspStarted = false;
    private Color peakColor = null;
    private int[] peaks = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT];
    private int[] peaksDelay = new int[DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT];
    private int peakDelay = DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY;
    private boolean peaksEnabled = true;
    private List visColors = null;
    private int barOffset = 1;
    private int width;
    private int height;
    private int height_2;
    // -- Spectrum analyser variables.
    private KJFFT fft;
    private float[] old_FFT;
    private int saFFTSampleSize;
    private int saBands;
    private float saColorScale;
    private float saMultiplier;
    private float saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY;
    private float sad;
    private SourceDataLine m_line = null;
    // -- VU Meter
    private float oldLeft;
    private float oldRight;
    //  private float vuAverage;
    //  private float vuSamples;
    private float vuDecay = DEFAULT_VU_METER_DECAY;
    private float vuColorScale;
    // -- FPS calulations.
    private long lfu = 0;
    private int fc = 0;
    private int fps = DEFAULT_FPS;
    private boolean showFPS = false;
    
    private AbsoluteConstraints constraints = null;

    //  private Runnable PAINT_SYNCHRONIZER = new AWTPaintSynchronizer();
    public SpectrumTimeAnalyzer()
    {
        setOpaque(false);
        initialize();
    }

    public void setConstraints(AbsoluteConstraints cnts)
    {
        constraints = cnts;
    }
    
    public AbsoluteConstraints getConstraints()
    {
        return constraints;
    }
    
    public boolean isPeaksEnabled()
    {
        return peaksEnabled;
    }

    public void setPeaksEnabled(boolean peaksEnabled)
    {
        this.peaksEnabled = peaksEnabled;
    }

    public int getFps()
    {
        return fps;
    }

    public void setFps(int fps)
    {
        this.fps = fps;
    }

    /**
     * Starts DSP.
     * @param line
     */
    public void startDSP(SourceDataLine line)
    {
        if (displayMode == DISPLAY_MODE_OFF) return;
        if (line != null) m_line = line;
        if (dsp == null)
        {
            dsp = new KJDigitalSignalProcessingAudioDataConsumer(2048, fps);
            dsp.add(this);
        }
        if ((dsp != null) && (m_line != null))
        {
            if (dspStarted == true)
            {
                stopDSP();
            }
            dsp.start(m_line);
            dspStarted = true;
            log.debug("DSP started");
        }
    }

    /**
     * Stop DSP.
     */
    public void stopDSP()
    {
        if (dsp != null)
        {
            dsp.stop();
            dspStarted = false;
            log.debug("DSP stopped");
        }
    }

    /**
     * Close DSP
     */
    public void closeDSP()
    {
        if (dsp != null)
        {
            stopDSP();
            dsp = null;
            log.debug("DSP closed");
        }
    }

    /**
     * Setup DSP.
     * @param line
     */
    public void setupDSP(SourceDataLine line)
    {
        if (dsp != null)
        {
            int channels = line.getFormat().getChannels();
            if (channels == 1) dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_MONO);
            else dsp.setChannelMode(KJDigitalSignalProcessingAudioDataConsumer.CHANNEL_MODE_STEREO);
            int bits = line.getFormat().getSampleSizeInBits();
            if (bits == 8) dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_EIGHT_BIT);
            else dsp.setSampleType(KJDigitalSignalProcessingAudioDataConsumer.SAMPLE_TYPE_SIXTEEN_BIT);
        }
    }

    /**
     * Write PCM data to DSP.
     * @param pcmdata
     */
    public void writeDSP(byte[] pcmdata)
    {
        if ((dsp != null) && (dspStarted == true)) dsp.writeAudioData(pcmdata);
    }

    /**
     * Return DSP.
     * @return
     */
    public KJDigitalSignalProcessingAudioDataConsumer getDSP()
    {
        return dsp;
    }

    /**
     * Set visual colors from skin.
     * @param viscolor
     */
    public void setVisColor(String viscolor)
    {
        ArrayList visColors = new ArrayList();
        viscolor = viscolor.toLowerCase();
        ByteArrayInputStream in = new ByteArrayInputStream(viscolor.getBytes());
        BufferedReader bin = new BufferedReader(new InputStreamReader(in));
        try
        {
            String line = null;
            while ((line = bin.readLine()) != null)
            {
                visColors.add(getColor(line));
            }
            Color[] colors = new Color[visColors.size()];
            visColors.toArray(colors);
            Color[] specColors = new Color[15];
            System.arraycopy(colors, 2, specColors, 0, 15);
            List specList = Arrays.asList(specColors);
            Collections.reverse(specList);
            specColors = (Color[]) specList.toArray(specColors);
            setSpectrumAnalyserColors(specColors);
            setBackground((Color) visColors.get(0));
            if (visColors.size()>23) setPeakColor((Color) visColors.get(23));
            if (visColors.size()>18) setScopeColor((Color) visColors.get(18));
        }
        catch (IOException ex)
        {
            log.warn("Cannot parse viscolors", ex);
        }
        finally
        {
            try
            {
                if (bin != null) bin.close();
            }
            catch (IOException e)
            {
            }
        }
    }

    /**
     * Set visual peak color.
     * @param c
     */
    public void setPeakColor(Color c)
    {
        peakColor = c;
    }

    /**
     * Set peak falloff delay.
     * @param framestowait
     */
    public void setPeakDelay(int framestowait)
    {
        int min = (int) Math.round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO - DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE) * fps);
        int max = (int) Math.round((DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO + DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO_RANGE) * fps);
        if ((framestowait >= min) && (framestowait <= max))
        {
            peakDelay = framestowait;
        }
        else
        {
            peakDelay = (int) Math.round(DEFAULT_SPECTRUM_ANALYSER_PEAK_DELAY_FPS_RATIO * fps);
        }
    }

    /**
     * Return peak falloff delay
     * @return int framestowait
     */
    public int getPeakDelay()
    {
        return peakDelay;
    }

    /**
     * Convert string to color.
     * @param linecolor
     * @return
     */
    public Color getColor(String linecolor)
    {
        Color color = Color.BLACK;
        StringTokenizer st = new StringTokenizer(linecolor, ",");
        int red = 0, green = 0, blue = 0;
        try
        {
            if (st.hasMoreTokens()) red = Integer.parseInt(st.nextToken().trim());
            if (st.hasMoreTokens()) green = Integer.parseInt(st.nextToken().trim());
            if (st.hasMoreTokens())
            {
                String blueStr = st.nextToken().trim();
                if (blueStr.length() > 3) blueStr = (blueStr.substring(0, 3)).trim();
                blue = Integer.parseInt(blueStr);
            }
            color = new Color(red, green, blue);
        }
        catch (NumberFormatException e)
        {
            log.debug("Cannot parse viscolor : "+e.getMessage());
        }
        return color;
    }

    private void computeColorScale()
    {
        saColorScale = ((float) spectrumAnalyserColors.length / height) * barOffset * 1.0f;
        vuColorScale = ((float) spectrumAnalyserColors.length / (width - 32)) * 2.0f;
    }

    private void computeSAMultiplier()
    {
        saMultiplier = (saFFTSampleSize / 2) / saBands;
    }

    private void drawScope(Graphics pGrp, float[] pSample)
    {
        pGrp.setColor(scopeColor);
        int wLas = (int) (pSample[0] * (float) height_2) + height_2;
        int wSt = 2;
        for (int a = wSt, c = 0; c < width; a += wSt, c++)
        {
            int wAs = (int) (pSample[a] * (float) height_2) + height_2;
            pGrp.drawLine(c, wLas, c + 1, wAs);
            wLas = wAs;
        }
    }

    private void drawSpectrumAnalyser(Graphics pGrp, float[] pSample, float pFrrh)
    {
        float c = 0;
        float[] wFFT = fft.calculate(pSample);
        float wSadfrr = (saDecay * pFrrh);
        float wBw = ((float) width / (float) saBands);
        for (int a = 0, bd = 0; bd < saBands; a += saMultiplier, bd++)
        {
            float wFs = 0;
            // -- Average out nearest bands.
            for (int b = 0; b < saMultiplier; b++)
            {
                wFs += wFFT[a + b];
            }
            // -- Log filter.
            wFs = (wFs * (float) Math.log(bd + 2));
            if (wFs > 1.0f)
            {
                wFs = 1.0f;
            }
            // -- Compute SA decay...
            if (wFs >= (old_FFT[a] - wSadfrr))
            {
                old_FFT[a] = wFs;
            }
            else
            {
                old_FFT[a] -= wSadfrr;
                if (old_FFT[a] < 0)
                {
                    old_FFT[a] = 0;
                }
                wFs = old_FFT[a];
            }
            drawSpectrumAnalyserBar(pGrp, (int) c, height, (int) wBw - 1, (int) (wFs * height), bd);
            c += wBw;
        }
    }

    private void drawVUMeter(Graphics pGrp, float[] pLeft, float[] pRight, float pFrrh)
    {
        if (displayMode == DISPLAY_MODE_OFF) return;
        float wLeft = 0.0f;
        float wRight = 0.0f;
        float wSadfrr = (vuDecay * pFrrh);
        for (int a = 0; a < pLeft.length; a++)
        {
            wLeft += Math.abs(pLeft[a]);
            wRight += Math.abs(pRight[a]);
        }
        wLeft = ((wLeft * 2.0f) / (float) pLeft.length);
        wRight = ((wRight * 2.0f) / (float) pRight.length);
        if (wLeft > 1.0f)
        {
            wLeft = 1.0f;
        }
        if (wRight > 1.0f)
        {
            wRight = 1.0f;
        }
        //      vuAverage += ( ( wLeft + wRight ) / 2.0f );
        //      vuSamples++;
        //
        //      if ( vuSamples > 128 ) {
        //          vuSamples /= 2.0f;
        //          vuAverage /= 2.0f;
        //      }
        if (wLeft >= (oldLeft - wSadfrr))
        {
            oldLeft = wLeft;
        }
        else
        {
            oldLeft -= wSadfrr;
            if (oldLeft < 0)
            {
                oldLeft = 0;
            }
        }
        if (wRight >= (oldRight - wSadfrr))
        {
            oldRight = wRight;
        }
        else
        {
            oldRight -= wSadfrr;
            if (oldRight < 0)
            {
                oldRight = 0;
            }
        }
        int wHeight = (height >> 1) - 24;
        drawVolumeMeterBar(pGrp, 16, 16, (int) (oldLeft * (float) (width - 32)), wHeight);
        //      drawVolumeMeterBar( pGrp, 16, wHeight + 22, (int)( ( vuAverage / vuSamples ) * (float)( width - 32 ) ), 4 );
        drawVolumeMeterBar(pGrp, 16, wHeight + 32, (int) (oldRight * (float) (width - 32)), wHeight);
        //      pGrp.fillRect( 16, 16, (int)( oldLeft  * (float)( width - 32 ) ), wHeight );
        //      pGrp.fillRect( 16, 64, (int)( oldRight * (float)( width - 32 ) ), wHeight );
    }

    private void drawSpectrumAnalyserBar(Graphics pGraphics, int pX, int pY, int pWidth, int pHeight, int band)
    {
        float c = 0;
        for (int a = pY; a >= pY - pHeight; a -= barOffset)
        {
            c += saColorScale;
            if (c < spectrumAnalyserColors.length)
            {
                pGraphics.setColor(spectrumAnalyserColors[(int) c]);
            }
            pGraphics.fillRect(pX, a, pWidth, 1);
        }
        if ((peakColor != null) && (peaksEnabled == true))
        {
            pGraphics.setColor(peakColor);
            if (pHeight > peaks[band])
            {
                peaks[band] = pHeight;
                peaksDelay[band] = peakDelay;
            }
            else
            {
                peaksDelay[band]--;
                if (peaksDelay[band] < 0) peaks[band]--;
                if (peaks[band] < 0) peaks[band] = 0;
            }
            pGraphics.fillRect(pX, pY - peaks[band], pWidth, 1);
        }
    }

    private void drawVolumeMeterBar(Graphics pGraphics, int pX, int pY, int pWidth, int pHeight)
    {
        float c = 0;
        for (int a = pX; a <= pX + pWidth; a += 2)
        {
            c += vuColorScale;
            if (c < 256.0f)
            {
                pGraphics.setColor(spectrumAnalyserColors[(int) c]);
            }
            pGraphics.fillRect(a, pY, 1, pHeight);
        }
    }

    private synchronized Image getDoubleBuffer()
    {
        if (bi == null || (bi.getWidth(null) != getSize().width || bi.getHeight(null) != getSize().height))
        {
            width = getSize().width;
            height = getSize().height;
            height_2 = height >> 1;
            computeColorScale();
            bi = getGraphicsConfiguration().createCompatibleVolatileImage(width, height);
        }
        return bi;
    }

    public static Color[] getDefaultSpectrumAnalyserColors()
    {
        Color[] wColors = new Color[256];
        for (int a = 0; a < 128; a++)
        {
            wColors[a] = new Color(0, (a >> 1) + 192, 0);
        }
        for (int a = 0; a < 64; a++)
        {
            wColors[a + 128] = new Color(a << 2, 255, 0);
        }
        for (int a = 0; a < 64; a++)
        {
            wColors[a + 192] = new Color(255, 255 - (a << 2), 0);
        }
        return wColors;
    }

    /**
     * @return Returns the current display mode, DISPLAY_MODE_SCOPE or DISPLAY_MODE_SPECTRUM_ANALYSER.
     */
    public int getDisplayMode()
    {
        return displayMode;
    }

    /**
     * @return Returns the current number of bands displayed by the spectrum analyser.
     */
    public int getSpectrumAnalyserBandCount()
    {
        return saBands;
    }

    /**
     * @return Returns the decay rate of the spectrum analyser's bands.
     */
    public float getSpectrumAnalyserDecay()
    {
        return saDecay;
    }

    /**
     * @return Returns the color the scope is rendered in.
     */
    public Color getScopeColor()
    {
        return scopeColor;
    }

    /**
     * @return Returns the color scale used to render the spectrum analyser bars.
     */
    public Color[] getSpectrumAnalyserColors()
    {
        return spectrumAnalyserColors;
    }

    private void initialize()
    {
        setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
        setBackground(DEFAULT_BACKGROUND_COLOR);
        prepareDisplayToggleListener();
        setSpectrumAnalyserBandCount(DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT);
        setSpectrumAnalyserFFTSampleSize(DEFAULT_SPECTRUM_ANALYSER_FFT_SAMPLE_SIZE);
    }

    /**
     * @return Returns 'true' if "Frames Per Second" are being calculated and displayed.
     */
    public boolean isShowingFPS()
    {
        return showFPS;
    }

    public void paintComponent(Graphics pGraphics)
    {
        if (displayMode == DISPLAY_MODE_OFF) return;
        if (dspStarted)
        {
            pGraphics.drawImage(getDoubleBuffer(), 0, 0, null);    
        }
        else
        {
            super.paintComponent(pGraphics);
        }
    }

    private void prepareDisplayToggleListener()
    {
        setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        addMouseListener(new MouseAdapter()
        {
            public void mouseClicked(MouseEvent pEvent)
            {
                if (pEvent.getButton() == MouseEvent.BUTTON1)
                {
                    if (displayMode + 1 > 1)
                    {
                        displayMode = 0;
                    }
                    else
                    {
                        displayMode++;
                    }
                }
            }
        });
    }

    /* (non-Javadoc)
     * @see kj.dsp.KJDigitalSignalProcessor#process(float[], float[], float)
     */
    public synchronized void process(float[] pLeft, float[] pRight, float pFrameRateRatioHint)
    {
        if (displayMode == DISPLAY_MODE_OFF) return;
        Graphics wGrp = getDoubleBuffer().getGraphics();
        wGrp.setColor(getBackground());
        wGrp.fillRect(0, 0, getSize().width, getSize().height);
        switch (displayMode)
        {
            case DISPLAY_MODE_SCOPE:
                drawScope(wGrp, stereoMerge(pLeft, pRight));
                break;
            case DISPLAY_MODE_SPECTRUM_ANALYSER:
                drawSpectrumAnalyser(wGrp, stereoMerge(pLeft, pRight), pFrameRateRatioHint);
                break;
            case DISPLAY_MODE_OFF:
                drawVUMeter(wGrp, pLeft, pRight, pFrameRateRatioHint);
                break;
        }
        // -- Show FPS if necessary.
        if (showFPS)
        {
            // -- Calculate FPS.
            if (System.currentTimeMillis() >= lfu + 1000)
            {
                lfu = System.currentTimeMillis();
                fps = fc;
                fc = 0;
            }
            fc++;
            wGrp.setColor(Color.yellow);
            wGrp.drawString("FPS: " + fps + " (FRRH: " + pFrameRateRatioHint + ")", 0, height - 1);
        }
        if (getGraphics() != null) getGraphics().drawImage(getDoubleBuffer(), 0, 0, null);
        //      repaint();
        //      try {
        //          EventQueue.invokeLater( new AWTPaintSynchronizer() );
        //      } catch ( Exception pEx ) {
        //          // -- Ignore exception.
        //          pEx.printStackTrace();
        //      }
    }

    /**
     * Sets the current display mode.
     *
     * @param pMode Must be either DISPLAY_MODE_SCOPE or DISPLAY_MODE_SPECTRUM_ANALYSER.
     */
    public synchronized void setDisplayMode(int pMode)
    {
        displayMode = pMode;
    }

    /**
     * Sets the color of the scope.
     *
     * @param pColor
     */
    public synchronized void setScopeColor(Color pColor)
    {
        scopeColor = pColor;
    }

    /**
     * When 'true' is passed as a parameter, will overlay the "Frames Per Seconds"
     * achieved by the component.
     *
     * @param pState
     */
    public synchronized void setShowFPS(boolean pState)
    {
        showFPS = pState;
    }

    /**
     * Sets the numbers of bands rendered by the spectrum analyser.
     *
     * @param pCount Cannot be more than half the "FFT sample size".
     */
    public synchronized void setSpectrumAnalyserBandCount(int pCount)
    {
        saBands = pCount;
        peaks = new int[saBands];
        peaksDelay = new int[saBands];
        computeSAMultiplier();
    }

    /**
     * Sets the spectrum analyser band decay rate.
     *
     * @param pDecay Must be a number between 0.0 and 1.0 exclusive.
     */
    public synchronized void setSpectrumAnalyserDecay(float pDecay)
    {
        if ((pDecay >= MIN_SPECTRUM_ANALYSER_DECAY) && (pDecay <= MAX_SPECTRUM_ANALYSER_DECAY))
        {
            saDecay = pDecay;
        }
        else saDecay = DEFAULT_SPECTRUM_ANALYSER_DECAY;
    }

    /**
     * Sets the spectrum analyser color scale.
     *
     * @param pColors Any amount of colors may be used. Must not be null.
     */
    public synchronized void setSpectrumAnalyserColors(Color[] pColors)
    {
        spectrumAnalyserColors = pColors;
        computeColorScale();
    }

    /**
     * Sets the FFT sample size to be just for calculating the spectrum analyser
     * values. The default is 512.
     *
     * @param pSize Cannot be more than the size of the sample provided by the DSP.
     */
    public synchronized void setSpectrumAnalyserFFTSampleSize(int pSize)
    {
        saFFTSampleSize = pSize;
        fft = new KJFFT(saFFTSampleSize);
        old_FFT = new float[saFFTSampleSize];
        computeSAMultiplier();
    }

    private float[] stereoMerge(float[] pLeft, float[] pRight)
    {
        for (int a = 0; a < pLeft.length; a++)
        {
            pLeft[a] = (pLeft[a] + pRight[a]) / 2.0f;
        }
        return pLeft;
    }

    /*public void update(Graphics pGraphics)
    {
        // -- Prevent AWT from clearing background.
        paint(pGraphics);
    }*/
}
