/**
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 *
 * 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 org.jivesoftware.sparkplugin.ui.components;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.CompoundControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.EnumControl;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.Port;
import javax.swing.AbstractButton;
import javax.swing.BoundedRangeModel;
import javax.swing.ButtonModel;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.DefaultButtonModel;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTree;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

/**
 * Pure Java Audio Mixer. Control Volume and Settings for any Sound device in the OS.
 *
 * @author Thiago Camargo
 */

public class JavaMixer {

    private static final Line.Info[] EMPTY_PORT_INFO_ARRAY = new Line.Info[0];
    DefaultMutableTreeNode root = new DefaultMutableTreeNode("Sound Mixers", true);
    JTree tree = new JTree(root);

    public JavaMixer() {
        List<Mixer> portMixers = getPortMixers();
        if (portMixers.size() == 0) System.err.println("No Mixers Found.");
        for (Mixer mixer : portMixers) {
            JavaMixer.MixerNode mixerNode = new JavaMixer.MixerNode(mixer);
            createMixerChildren(mixerNode);
            root.add(mixerNode);
        }
    }

    public JTree getTree() {
        return tree;
    }

    public Component getPrefferedMasterVolume() {
        TreePath path = findByName(new TreePath(root), new String[]{"SPEAKER", "Volume"});

        if (path == null) {
            path = findByName(new TreePath(root), new String[]{"Master target", "Master", "Mute"});
        }

        if (path != null) {
            if (path.getLastPathComponent() instanceof JavaMixer.ControlNode)
                return ((JavaMixer.ControlNode) path.getLastPathComponent()).getComponent();
        }
        return null;
    }

    public Component getPrefferedInputVolume() {
        TreePath path = findByName(new TreePath(root), new String[]{"MICROPHONE", "Volume"});

        if (path == null) {
            path = findByName(new TreePath(root), new String[]{"Capture source", "Capture", "Volume"});
        }

        if (path != null) {
            if (path.getLastPathComponent() instanceof JavaMixer.ControlNode)
                return ((JavaMixer.ControlNode) path.getLastPathComponent()).getComponent();
        }
        return null;
    }

    public void setMicrophoneInput() {
        TreePath path = findByName(new TreePath(root), new String[]{"MICROPHONE", "Select"});

        if (path == null) {
            path = findByName(new TreePath(root), new String[]{"Capture source", "Capture", "Mute"});
        }

        if (path != null) {
            if (path.getLastPathComponent() instanceof JavaMixer.ControlNode) {
                BooleanControl bControl = (BooleanControl) (((JavaMixer.ControlNode) path.getLastPathComponent()).getControl());
                bControl.setValue(true);
            }
        }
    }

    public void setMuteForMicrophoneOutput() {
        TreePath path = findByName(new TreePath(root), new String[]{"SPEAKER", "Microfone", "Mute"});

        if (path == null) {
            path = findByName(new TreePath(root), new String[]{"MIC target", "mic", "Mute"});
        }

        if (path != null) {
            if (path.getLastPathComponent() instanceof JavaMixer.ControlNode) {
                BooleanControl bControl = (BooleanControl) (((JavaMixer.ControlNode) path.getLastPathComponent()).getControl());
                bControl.setValue(true);
            }
        }
    }

    /**
     * Returns the Mixers that support Port lines.
     *
     * @return List<Mixer> Port Mixers
     */
    private List<Mixer> getPortMixers() {
        List<Mixer> supportingMixers = new ArrayList<Mixer>();
        Mixer.Info[] aMixerInfos = AudioSystem.getMixerInfo();
        for (Mixer.Info aMixerInfo : aMixerInfos) {
            Mixer mixer = AudioSystem.getMixer(aMixerInfo);
            boolean bSupportsPorts = arePortsSupported(mixer);
            if (bSupportsPorts) {
                supportingMixers.add(mixer);
            }
        }
        return supportingMixers;
    }

    private boolean arePortsSupported(Mixer mixer) {
        Line.Info[] infos;
        infos = mixer.getSourceLineInfo();
        for (Line.Info info : infos) {
            if (info instanceof Port.Info) {
                return true;
            } else if (info instanceof DataLine.Info) {
                return true;
            }
        }
        infos = mixer.getTargetLineInfo();
        for (Line.Info info : infos) {
            if (info instanceof Port.Info) {
                return true;
            } else if (info instanceof DataLine.Info) {
                return true;
            }
        }
        return false;
    }

    private void createMixerChildren(JavaMixer.MixerNode mixerNode) {
        Mixer mixer = mixerNode.getMixer();       
        Line.Info[] infosToCheck = getPortInfo(mixer);
        
        for (Line.Info anInfosToCheck : infosToCheck) {
            if (mixer.isLineSupported(anInfosToCheck)) {
                Port port = null;
                DataLine dLine = null;

                int maxLines = mixer.getMaxLines(anInfosToCheck);
                // Workaround to prevent a JVM crash on Mac OS X (Intel) 1.5.0_07 JVM
                if (maxLines > 0) {
                    try {
                        if (anInfosToCheck instanceof Port.Info) {
                            port = (Port) mixer.getLine(anInfosToCheck);
                            port.open();
                        }
                        else if (anInfosToCheck instanceof DataLine.Info) {
                            dLine = (DataLine) mixer.getLine(anInfosToCheck);
                            if (!dLine.isOpen()) {
                                dLine.open();
                            }
                        }
                    }
                    catch (LineUnavailableException e) {
                        // Do Nothing
                    }
                    catch (Exception e) {
                        // Do Nothing
                    }
                }
                if (port != null) {
                    JavaMixer.PortNode portNode = new JavaMixer.PortNode(port);
                    createPortChildren(portNode);
                    mixerNode.add(portNode);
                } else if (dLine != null) {
                    JavaMixer.PortNode portNode = new JavaMixer.PortNode(dLine);
                    createPortChildren(portNode);
                    mixerNode.add(portNode);
                }
            }
        }
    }

    private Line.Info[] getPortInfo(Mixer mixer) {
        Line.Info[] infos;
        List<Line.Info> portInfoList = new ArrayList<Line.Info>();
        infos = mixer.getSourceLineInfo();
        for (Line.Info info : infos) {
            if (info instanceof Port.Info || info instanceof DataLine.Info) {
                portInfoList.add((Line.Info) info);
            }
        }
        infos = mixer.getTargetLineInfo();
        for (Line.Info info1 : infos) {
            if (info1 instanceof Port.Info || info1 instanceof DataLine.Info) {
                portInfoList.add((Line.Info) info1);
            }
        }
        return portInfoList.toArray(EMPTY_PORT_INFO_ARRAY);
    }

    private void createPortChildren(JavaMixer.PortNode portNode) {
        Control[] aControls = portNode.getPort().getControls();
        for (Control aControl : aControls) {
            JavaMixer.ControlNode controlNode = new JavaMixer.ControlNode(aControl);
            createControlChildren(controlNode);
            portNode.add(controlNode);
        }
    }

    private void createControlChildren(JavaMixer.ControlNode controlNode) {
        if (controlNode.getControl() instanceof CompoundControl) {
            CompoundControl control = (CompoundControl) controlNode.getControl();
            Control[] aControls = control.getMemberControls();
            for (Control con : aControls) {
                JavaMixer.ControlNode conNode = new JavaMixer.ControlNode(con);
                createControlChildren(conNode);
                controlNode.add(conNode);
            }
        }
    }

    /**
     * Returns whether the type of a FloatControl is BALANCE or PAN.
     *
     * @param control FloatControl control
     * @return boolean is Balance or Pan
     */
    private static boolean isBalanceOrPan(FloatControl control) {
        Control.Type type = control.getType();
        return type.equals(FloatControl.Type.PAN) || type.equals(FloatControl.Type.BALANCE);
    }

    public class MixerNode extends DefaultMutableTreeNode {

	private static final long serialVersionUID = 5514718793497318648L;
	Mixer mixer;

        public MixerNode(Mixer mixer) {
            super(mixer.getMixerInfo(), true);
            this.mixer = mixer;
        }

        public Mixer getMixer() {
            return mixer;
        }

    }

    public class PortNode extends DefaultMutableTreeNode {

	private static final long serialVersionUID = -4388437363589688167L;
	Line port;

        public PortNode(Line port) {
            super(port.getLineInfo(), true);
            this.port = port;
        }

        public Line getPort() {
            return port;
        }

    }

    public class ControlNode extends DefaultMutableTreeNode {

	private static final long serialVersionUID = 8840473247266403685L;
	Control control;
        Component component;

        public ControlNode(Control control) {
            super(control.getType(), true);
            this.control = control;
            if (control instanceof BooleanControl) {
                component = createControlComponent((BooleanControl) control);
            } else if (control instanceof EnumControl) {
                component = createControlComponent((EnumControl) control);
            } else if (control instanceof FloatControl) {
                component = createControlComponent((FloatControl) control);
            } else {
                component = null;
            }
        }

        public Control getControl() {
            return control;
        }

        public Component getComponent() {
            return component;
        }

        private JComponent createControlComponent(BooleanControl control) {
            AbstractButton button;
            String strControlName = control.getType().toString();
            ButtonModel model = new JavaMixer.BooleanControlButtonModel(control);
            button = new JCheckBox(strControlName);
            button.setModel(model);
            return button;
        }

        private JComponent createControlComponent(EnumControl control) {
            JPanel component = new JPanel();
            String strControlName = control.getType().toString();
            component.setBorder(new TitledBorder(new EtchedBorder(), strControlName));
            return component;
        }

        private JComponent createControlComponent(FloatControl control) {
            int orientation = isBalanceOrPan(control) ? JSlider.HORIZONTAL : JSlider.VERTICAL;
            BoundedRangeModel model = new JavaMixer.FloatControlBoundedRangeModel(control);
            JSlider slider = new JSlider(model);
            slider.setOrientation(orientation);
            slider.setPaintLabels(true);
            slider.setPaintTicks(true);
            slider.setSize(10, 50);
            return slider;
        }

    }

    public class BooleanControlButtonModel extends DefaultButtonModel {
	private static final long serialVersionUID = -8264153878420797906L;
	private BooleanControl control;

        public BooleanControlButtonModel(BooleanControl control) {
            this.control = control;
            this.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    setSelected(!isSelected());
                }
            });
        }

        public void setSelected(boolean bSelected) {
            control.setValue(bSelected);
        }

        public boolean isSelected() {
            return control.getValue();
        }
    }

    public class FloatControlBoundedRangeModel extends DefaultBoundedRangeModel {

	private static final long serialVersionUID = 7826447995854231838L;
	private FloatControl control;
        private float factor;

        public FloatControlBoundedRangeModel(FloatControl control) {
            this.control = control;
            float range = 100;
            float steps = range / 100;
            factor = range / steps;
            int min = (int) (control.getMinimum() * factor);
            int max = (int) (control.getMaximum() * factor);
            int value = (int) (control.getValue() * factor);
            setRangeProperties(value, 0, min, max, false);
        }

        private float getScaleFactor() {
            return factor;
        }

        public void setValue(int nValue) {
            super.setValue(nValue);
            control.setValue((float) nValue / getScaleFactor());
        }

        public int getValue() {
            return (int) (control.getValue() * getScaleFactor());
        }

    }

    public static void main(String[] args) {
        final JavaMixer sm = new JavaMixer();
        final JFrame jf = new JFrame("Mixer Test");
        final JPanel jp = new JPanel();
        jf.add(jp);
        jp.add(sm.getTree());
        jf.setSize(600, 500);
        jf.setVisible(true);
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        sm.getTree().addTreeSelectionListener(new TreeSelectionListener() {
            public void valueChanged(TreeSelectionEvent e) {
                TreePath path = e.getPath();
                if (path.getLastPathComponent() instanceof JavaMixer.ControlNode) {
                    JavaMixer.ControlNode controlNode = (JavaMixer.ControlNode) path.getLastPathComponent();
                    if (!(controlNode.getControl() instanceof CompoundControl)) {
                        if (jp.getComponentCount() > 1)
                            jp.remove(1);
                        jp.add(controlNode.getComponent(), 1);
                        jp.repaint();
                    }
                }
            }
        });
        jp.add(sm.getPrefferedMasterVolume());
        jp.add(sm.getPrefferedMasterVolume());
        jp.add(sm.getPrefferedInputVolume());
        jp.repaint();
        sm.setMicrophoneInput();
        sm.setMuteForMicrophoneOutput();
    }

    public TreePath find(TreePath path, Object[] nodes) {
        return find2(path, nodes, 0, false);
    }

    public TreePath findByName(TreePath path, String[] names) {
        return find2(path, names, 0, true);
    }

    private TreePath find2(TreePath parent, Object[] nodes, int depth, boolean byName) {
        TreeNode node = (TreeNode) parent.getLastPathComponent();
        if (depth > nodes.length - 1) {
            return parent;
        }

        if (node.getChildCount() >= 0) {
            for (Enumeration<TreeNode> e = node.children(); e.hasMoreElements();) {
                TreeNode n = e.nextElement();
                TreePath path = parent.pathByAddingChild(n);
                boolean find;

                if (byName) {
                    find = n.toString().toUpperCase().indexOf(nodes[depth].toString().toUpperCase()) > -1;
                } else {
                    find = n.equals(nodes[depth]);
                }

                if (find) {
                    TreePath result = find2(path, nodes, depth + 1, byName);
                    if (result != null) {
                        return result;
                    }
                } else {
                    TreePath result = find2(path, nodes, depth, byName);
                    if (result != null) {
                        return result;
                    }
                }
            }
        }

        return null;
    }
}
