public class LibraryHelper {
	private static final HashSet<String> avcDependencies = new HashSet<String>();
	private static final boolean needsDependencyExtraction;
	private static final String libraryExtractionFolder;

	private static boolean librariesExtracted = false;
	
	static {
		needsDependencyExtraction = System.getProperty("os.name").contains("Windows");
		libraryExtractionFolder = System.getProperty("java.io.tmpdir", ".") + File.separatorChar + UUID.randomUUID().toString();
		
		// AVC dependencies
		if (System.getProperty("os.name").contains("Windows")) {
			avcDependencies.add("avutil-54");
			avcDependencies.add("swresample-1");
//			avcDependencies.add("avdevice-56");
			avcDependencies.add("swscale-3");
			avcDependencies.add("avcodec-56");
//			avcDependencies.add("avfilter-5");
//			avcDependencies.add("avformat-56");

			avcDependencies.add("pthreadVC2");
		}
		
		// The AVC JNI library itself
		avcDependencies.add("nv_avc_dec");
	}
	
	public static void loadNativeLibrary(String libraryName) {
		if (librariesExtracted && avcDependencies.contains(libraryName)) {
			System.load(libraryExtractionFolder + File.separatorChar + System.mapLibraryName(libraryName));
		}
		else {
			System.loadLibrary(libraryName);
		}
	}

	public static void prepareNativeLibraries() {
		if (!needsDependencyExtraction || !isRunningFromJar()) {
			return;
		}
		
		// Create the native library folder
		new File(libraryExtractionFolder).mkdirs();
		
		try {
			for (String dependency : avcDependencies) {
				extractNativeLibrary(dependency);
			}
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		librariesExtracted = true;
	}
	
	public static String getRunningPlatformString() {
		String osName = System.getProperty("os.name", "");
		String jreArch = System.getProperty("os.arch", "");
		StringBuilder str = new StringBuilder();
		
		// OS X is a single-arch platform
		if (osName.contains("Mac OS X")) {
			return "osx";
		}
		
		// Windows and Linux are dual-arch
		if (osName.contains("Windows")) {
			str.append("win");
		}
		else if (osName.contains("Linux")) {
			str.append("lin");
		}
		else {
			return null;
		}
		
		// Check OS arch
		if (jreArch.equalsIgnoreCase("x86") ||
			jreArch.equalsIgnoreCase("i386") ||
			jreArch.equalsIgnoreCase("i686")) {
			str.append("32");
		}
		else {
			str.append("64");
		}
		
		return str.toString();
	}
	
	public static String getLibraryPlatformString() throws IOException {
		BufferedReader reader;
		
		if (isRunningFromJar()) {
			reader = getPlatformInJar();
		}
		else {
			reader = getPlatformUnpackaged();
		}
		
		return reader.readLine();
	}
	
	private static BufferedReader getPlatformUnpackaged() throws FileNotFoundException {
		String platformPath = System.getProperty("java.library.path", "") + File.separator + "platform";
		InputStream inStream = new FileInputStream(new File(platformPath));
		return new BufferedReader(new InputStreamReader(inStream));
	}
	
	private static BufferedReader getPlatformInJar() throws FileNotFoundException {
		InputStream platformFile = new Object().getClass().getResourceAsStream("/binlib/platform");
		if (platformFile == null) {
			throw new FileNotFoundException("Unable to find platform designation in JAR");
		}
		
		return new BufferedReader(new InputStreamReader(platformFile));
	}
	
	private static void extractNativeLibrary(String libraryName) throws IOException {
		// convert general library name to platform-specific name
		libraryName = System.mapLibraryName(libraryName);
		
		InputStream resource = new Object().getClass().getResourceAsStream("/binlib/"+libraryName);
		if (resource == null) {
			throw new FileNotFoundException("Unable to find native library in JAR: "+libraryName);
		}
		File destination = new File(libraryExtractionFolder+File.separatorChar+libraryName);
		
		// this will only delete it if it exists, and then create a new file
		destination.delete();
		destination.createNewFile();
		
		// schedule the temporary file to be deleted when the program exits
		destination.deleteOnExit();
		
		//this is the janky java 6 way to copy a file
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(destination);
			int read;
			byte[] readBuffer = new byte[16384];
			while ((read = resource.read(readBuffer)) != -1) {
				fos.write(readBuffer, 0, read);
			}
		} finally {
			if (fos != null) {
				fos.close();
			}
		}
	}
	
	public static boolean isRunningFromJar() {
		String classPath = LibraryHelper.class.getResource("LibraryHelper.class").toString();
		return classPath.startsWith("jar:");
	}
} public class PlatformBinding {
    /**
     * Gets an instance of a video decoder/renderer.
     *
     * @return a video decoder and renderer
     */
    public static VideoDecoderRenderer getVideoDecoderRenderer() {
    	return new GLDecoderRenderer();
    }

    /**
     * Gets the name of this device.
     * <br>Currently, the hostname of the system.
     *
     * @return the name of this device
     */
    public static String getDeviceName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            return "LimelightPC";
        }
    }

    /**
     * Gets an instance of an audio decoder/renderer.
     *
     * @return an audio decoder and renderer
     */
    public static AudioRenderer getAudioRenderer() {
        return new JavaxAudioRenderer();
    }

    /**
     * Gets an instance of a crypto provider
     *
     * @return a PcCryptoProvider object
     */
    public static LimelightCryptoProvider getCryptoProvider() {
        return new PcCryptoProvider();
    }
} public class MainFrame {
	private JTextField hostField;
	private JButton pair;
	private JButton stream;
	private JFrame limeFrame;
	private JComboBox<String> mdnsHostList;

    private HashMap<String, InetAddress> mdnsHosts;
    private MdnsDiscoveryAgent mdnsAgent;

	/**
	 * Gets the actual JFrame this class creates
	 * @return the JFrame that is the main frame
	 */
	public JFrame getLimeFrame() {
		return limeFrame;
	}

	/*
	 * Creates the menu bar for the user to go to preferences, mappings, etc.
	 */
	private JMenuBar createMenuBar() {
		JMenuBar menuBar = new JMenuBar();
		JMenu optionsMenu = new JMenu("Options");
		JMenuItem gamepadSettings = new JMenuItem("Gamepad Settings");
		JMenuItem generalSettings = new JMenuItem("Preferences");
		generalSettings.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_COMMA, (Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())));
		
		gamepadSettings.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				new GamepadConfigFrame().build();
			}
		});

		generalSettings.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				new PreferencesFrame().build();
			}
		});

		optionsMenu.add(gamepadSettings);
		optionsMenu.add(generalSettings);

		menuBar.add(optionsMenu);

		return menuBar;
	}

	/*
	 * Creates the listener for the stream button- starts the stream process
	 */
	private ActionListener createStreamButtonListener() {
		return new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				String host = hostField.getText();
				Preferences prefs = PreferencesManager.getPreferences();
				if (!host.equals(prefs.getHost())) {
					prefs.setHost(host);
					PreferencesManager.writePreferences(prefs);
				}
				// Limelight.createInstance(host);
				showApps();
			}
		};
	}

	/*
	 * Creates the listener for the pair button- requests a pairing with the specified host
	 */
	private ActionListener createPairButtonListener() {
		return new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				new Thread(new Runnable() {
					public void run() {
						Preferences prefs = PreferencesManager.getPreferences();
						
						// Save preferences to preserve possibly new unique ID
						PreferencesManager.writePreferences(prefs);
						
						String msg = Limelight.pair(prefs.getUniqueId(), hostField.getText());
						Limelight.displayUiMessage(limeFrame, msg, "Moonlight", JOptionPane.INFORMATION_MESSAGE);
					}
				}).start();
			}
		};
	}

    /**
     * Builds all components of the frame, including the frame itself and displays it to the user.
     */
    public void build() {
        limeFrame = new JFrame("Moonlight");
        limeFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container mainPane = limeFrame.getContentPane();

        mainPane.setLayout(new BorderLayout());

        JPanel centerPane = new JPanel();
        centerPane.setLayout(new BoxLayout(centerPane, BoxLayout.Y_AXIS));

        Preferences prefs = PreferencesManager.getPreferences();

        hostField = new JTextField();
        hostField.setMaximumSize(new Dimension(Integer.MAX_VALUE, 24));
        hostField.setToolTipText("Enter host name or IP address");
        hostField.setText(prefs.getHost());
        hostField.setSelectionStart(0);
        hostField.setSelectionEnd(hostField.getText().length());

        stream = new JButton("Show App List");
        stream.addActionListener(createStreamButtonListener());

        pair = new JButton("Pair");
        pair.addActionListener(createPairButtonListener());
        pair.setToolTipText("Send pair request to GeForce PC");

        mdnsHosts = new HashMap<String, InetAddress>();
        mdnsHostList = new JComboBox<String>();
        mdnsHostList.addItem("Choose a local PC...");
        
        mdnsAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
			@Override
			public void notifyComputerAdded(MdnsComputer computer) {
                if (!mdnsHosts.containsKey(computer.getName())) {
                    mdnsHosts.put(computer.getName(), computer.getAddress());
                    mdnsHostList.addItem(computer.getName());
                }
			}

			@Override
			public void notifyComputerRemoved(MdnsComputer computer) {
                // We'll keep any host we've seen before around, as users will find it convenient
				LimeLog.info("Computer lost: "+computer.getName());
			}

			@Override
			public void notifyDiscoveryFailure(Exception e) {
				LimeLog.warning("Discovery failure");
				e.printStackTrace();
			}
        	
        });
        mdnsAgent.startDiscovery(1000);

        // Propagate selections from mDNS to the hosts field
        mdnsHostList.addItemListener(new ItemListener() {
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED && mdnsHostList.getSelectedIndex() != 0) {
                    hostField.setText((String) mdnsHosts.get(mdnsHostList.getSelectedItem()).getHostAddress());
                }
            }
        });


        Box streamBox = Box.createHorizontalBox();
        streamBox.add(Box.createHorizontalGlue());
        streamBox.add(stream);
        streamBox.add(Box.createHorizontalGlue());

        Box pairBox = Box.createHorizontalBox();
        pairBox.add(Box.createHorizontalGlue());
        pairBox.add(pair);
        pairBox.add(Box.createHorizontalGlue());

        Box hostBox = Box.createHorizontalBox();
        hostBox.add(Box.createHorizontalStrut(20));
        hostBox.add(hostField);
        hostBox.add(Box.createHorizontalStrut(20));

        Box mdnsBox = Box.createHorizontalBox();
        mdnsBox.add(Box.createHorizontalStrut(20));
        mdnsBox.add(mdnsHostList);
        mdnsBox.add(Box.createHorizontalStrut(20));

        Box contentBox = Box.createVerticalBox();
        contentBox.add(Box.createVerticalStrut(20));
        contentBox.add(hostBox);
        contentBox.add(Box.createVerticalStrut(5));
        contentBox.add(mdnsBox);
        contentBox.add(Box.createVerticalStrut(10));
        contentBox.add(streamBox);
        contentBox.add(Box.createVerticalStrut(7));
        contentBox.add(pairBox);
        contentBox.add(Box.createVerticalGlue());

        centerPane.add(contentBox);
        mainPane.add(centerPane, "Center");
        Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();

        limeFrame.setJMenuBar(createMenuBar());
        limeFrame.getRootPane().setDefaultButton(stream);
        limeFrame.setSize(300, 230);
        limeFrame.setLocation(dim.width / 2 - limeFrame.getSize().width / 2,
                              dim.height / 2 - limeFrame.getSize().height / 2);
        limeFrame.setResizable(false);
        limeFrame.setVisible(true);
    }

    private void showApps() {
        String host = hostField.getText();
        AppsFrame appsFrame = new AppsFrame(host);
        appsFrame.build();
    }
} public class StreamFrame extends JFrame {
	private static final long serialVersionUID = 1L;
	
	private static final double DESIRED_ASPECT_RATIO = 16.0/9.0;
	private static final double ALTERNATE_ASPECT_RATIO = 16.0/10.0;
	
	private KeyboardHandler keyboard;
	private MouseHandler mouse;
	private JProgressBar spinner;
	private JLabel spinnerLabel;
	private Cursor noCursor;
	private Limelight limelight;
	private RenderPanel renderingSurface;
	private Preferences userPreferences;

	/**
	 * Frees the mouse ie. makes it visible and allowed to move outside the frame.
	 */
	public void freeMouse() {
		mouse.free();
		showCursor();
	}

	/**
	 * Captures the mouse ie. makes it invisible and not allowed to leave the frame
	 */
	public void captureMouse() {
		mouse.capture();
		hideCursor();
	}

	/**
	 * Builds the components of this frame with the specified configurations.
	 * @param conn the connection this frame belongs to
	 * @param streamConfig the configurations for this frame
	 * @param fullscreen if the frame should be made fullscreen
	 */
	public void build(Limelight limelight, NvConnection conn, StreamConfiguration streamConfig, Preferences prefs) {
		this.limelight = limelight;
		this.userPreferences = prefs;
		
		keyboard = new KeyboardHandler(conn, this);
		mouse = new MouseHandler(conn, this);

		this.setBackground(Color.BLACK);
		this.setFocusableWindowState(true);
		this.setFocusTraversalKeysEnabled(false);
		this.addWindowListener(createWindowListener());
		
		Container contentPane = this.getContentPane();
		
		renderingSurface = new RenderPanel();
		renderingSurface.addKeyListener(keyboard);
		renderingSurface.addMouseListener(mouse);
		renderingSurface.addMouseMotionListener(mouse);
		renderingSurface.addMouseWheelListener(mouse);
		renderingSurface.setBackground(Color.BLACK);
		renderingSurface.setFocusable(true);
		renderingSurface.setLayout(new BoxLayout(renderingSurface, BoxLayout.Y_AXIS));
		renderingSurface.setVisible(true);
		renderingSurface.setFocusTraversalKeysEnabled(false);
		
        addComponentListener(new ComponentListener() {
			@Override
			public void componentHidden(ComponentEvent arg0) {}
			@Override
			public void componentMoved(ComponentEvent arg0) {}
			@Override
			public void componentResized(ComponentEvent arg0) {
				renderingSurface.setSize(getContentPane().getSize());
			}
			@Override
			public void componentShown(ComponentEvent arg0) {}
        });
		
		contentPane.setLayout(new BorderLayout());
		contentPane.add(renderingSurface, "Center");
		
		if (userPreferences.getFullscreen()) {
			makeFullScreen(streamConfig, userPreferences.getAllowResolutionChange());
			
			// OS X hack for full-screen losing focus
			if (System.getProperty("os.name").contains("Mac OS X")) {
				this.setVisible(false);
				this.setVisible(true);
			}
		}
		else {
			this.setVisible(true);
			
			// Only fill the available screen area (excluding taskbar, etc)
			Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(getGraphicsConfiguration());
			Insets windowInsets = this.getInsets();
			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
			
			int windowInsetWidth = windowInsets.left + windowInsets.right;
			int windowInsetHeight = windowInsets.top + windowInsets.bottom;
			int maxWidth = screenSize.width - (screenInsets.left + screenInsets.right);
			int maxHeight = screenSize.height - (screenInsets.top + screenInsets.bottom);
			this.setSize(new Dimension(Math.min(streamConfig.getWidth() + windowInsetWidth, maxWidth),
				Math.min(streamConfig.getHeight() + windowInsetHeight, maxHeight)));
			
			// Maximize the window if the window size would be larger than the usable screen area
			if (streamConfig.getWidth() + windowInsetWidth > maxWidth &&
					streamConfig.getHeight() + windowInsetHeight > maxHeight) {
				setExtendedState(JFrame.MAXIMIZED_BOTH);
			}
		}
		
		renderingSurface.setSize(getContentPane().getSize());

		hideCursor();
	}
	
	private ArrayList<DisplayMode> getDisplayModesByAspectRatio(DisplayMode[] configs, double aspectRatio) {
		ArrayList<DisplayMode> matchingConfigs = new ArrayList<DisplayMode>();
		
		for (DisplayMode config : configs) {
			double configAspectRatio = (double)config.getWidth()/(double)config.getHeight();
			if (Math.abs(configAspectRatio - aspectRatio) < 0.01) {
				matchingConfigs.add(config);
			}
		}
		
		return matchingConfigs;
	}
	
	private DisplayMode getBestDisplay(StreamConfiguration targetConfig, DisplayMode[] configs) {
		int targetDisplaySize = targetConfig.getWidth()*targetConfig.getHeight();
		
		// Try to match the target aspect ratio
		ArrayList<DisplayMode> aspectMatchingConfigs = getDisplayModesByAspectRatio(configs, DESIRED_ASPECT_RATIO);
		if (aspectMatchingConfigs.size() == 0) {
			// No matches for the target, so try the alternate
			aspectMatchingConfigs = getDisplayModesByAspectRatio(configs, ALTERNATE_ASPECT_RATIO);
			if (aspectMatchingConfigs.size() == 0) {
				// No matches for either, so just use all of them
				aspectMatchingConfigs = new ArrayList<DisplayMode>(Arrays.asList(configs));
			}
		}
		
		// Sort by display size
		Collections.sort(aspectMatchingConfigs, new Comparator<DisplayMode>() {
			public int compare(DisplayMode o1, DisplayMode o2) {
				if (o1.getWidth()*o1.getHeight() > o2.getWidth()*o2.getHeight()) {
					return -1;
				} else if (o2.getWidth()*o2.getHeight() > o1.getWidth()*o1.getHeight()) {
					return 1;
				} else {
					return 0;
				}
			}
		});

		// Find the aspect-matching config with the closest matching display size
		DisplayMode bestConfig = null;
		for (DisplayMode config : aspectMatchingConfigs) {
			if (config.getWidth()*config.getHeight() >= targetDisplaySize) {
				bestConfig = config;
			}
		}
		
		if (bestConfig != null) {
			LimeLog.info("Using full-screen display mode "+bestConfig.getWidth()+"x"+bestConfig.getHeight()+
					" for "+targetConfig.getWidth()+"x"+targetConfig.getHeight()+" stream");
		} else {
			bestConfig = aspectMatchingConfigs.get(0);
			LimeLog.info("No matching display modes. Using largest: " +bestConfig.getWidth()+"x"+bestConfig.getHeight()+
					" for "+targetConfig.getWidth()+"x"+targetConfig.getHeight()+" stream");
		}
		
		return bestConfig;
	}
	
	private void makeFullScreen(StreamConfiguration streamConfig, boolean allowResolutionChange) {
		GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
		if (gd.isFullScreenSupported()) {
			this.setResizable(false);
			this.setUndecorated(true);
			gd.setFullScreenWindow(this);

			if (allowResolutionChange) {
				if (gd.isDisplayChangeSupported()) {
					DisplayMode config = getBestDisplay(streamConfig, gd.getDisplayModes());
					if (config != null) {
						gd.setDisplayMode(config);
					}
				} else {
					Limelight.displayUiMessage(
							this,
							"Unable to change display resolution. \nThis may not be the correct resolution",
							"Display Resolution",
							JOptionPane.ERROR_MESSAGE);
				}
			}
		} else {
			Limelight.displayUiMessage(
					this, 
					"Your operating system does not support fullscreen.", 
					"Fullscreen Unsupported",
					JOptionPane.ERROR_MESSAGE);
		}
	}
	
	/**
	 * Makes the mouse cursor invisible
	 */
	public void hideCursor() {
		if (noCursor == null) {
			// Transparent 16 x 16 pixel cursor image.
			BufferedImage cursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);

			// Create a new blank cursor.
			noCursor = Toolkit.getDefaultToolkit().createCustomCursor(
					cursorImg, new Point(0, 0), "blank cursor");
		}
		
		for (Component c : getContentPane().getComponents()) {
			c.setCursor(noCursor);
		}
		
		setCursor(noCursor);
	}

	/**
	 * Makes the mouse cursor visible
	 */
	public void showCursor() {
		for (Component c : getContentPane().getComponents()) {
			c.setCursor(Cursor.getDefaultCursor());
		}
		
		setCursor(Cursor.getDefaultCursor());
	}

	/**
	 * Shows a progress bar with a label underneath that tells the user what 
	 * loading stage the stream is at.
	 * @param stage the currently loading stage
	 */
	public void showSpinner(Stage stage) {
		if (spinner == null) {
			spinner = new JProgressBar();
			spinner.setIndeterminate(true);
			spinner.setMaximumSize(new Dimension(150, 30));

			spinnerLabel = new JLabel();
			spinnerLabel.setForeground(Color.white);

			Box spinBox = Box.createHorizontalBox();
			spinBox.add(Box.createHorizontalGlue());
			spinBox.add(spinner);
			spinBox.add(Box.createHorizontalGlue());

			Box lblBox = Box.createHorizontalBox();
			lblBox.add(Box.createHorizontalGlue());
			lblBox.add(spinnerLabel);
			lblBox.add(Box.createHorizontalGlue());

			renderingSurface.add(Box.createVerticalGlue());
			renderingSurface.add(spinBox);
			renderingSurface.add(Box.createVerticalStrut(10));
			renderingSurface.add(lblBox);
			renderingSurface.add(Box.createVerticalGlue());
		}
		spinnerLabel.setText("Starting " + stage.getName() + "...");
	}
	
	/**
	 * Creates the listener for the window.
	 * It terminates the connection when the window is closed
	 */
	private WindowListener createWindowListener() {
		return new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				close();
			}
		};
	}

	/**
	 * Hides the spinner and the label
	 */
	public void hideSpinner() {
		renderingSurface.removeAll();
		renderingSurface.requestFocus();
	}

	/**
	 * Stops the stream and destroys the frame
	 */
	public void close() {
		limelight.stop();
		dispose();
		if (Limelight.COMMAND_LINE_LAUNCH) {
			System.exit(0);
		}
	}
    
    public RenderPanel getRenderingSurface() {
        return renderingSurface;
    }
    
    public Preferences getUserPreferences() {
    	return this.userPreferences;
    }
} public class GamepadHandler implements DeviceListener {

	private class Gamepad {
		public short controllerNumber;
		
		public GamepadMapping mapping; 
		
		public short buttonFlags;
		public byte leftTrigger;
		public byte rightTrigger;
		public short rightStickX;
		public short rightStickY;
		public short leftStickX;
		public short leftStickY;
		
		public void assignControllerNumber() {
			for (short i = 0; i < 4; i++) {
				if ((currentControllers & (1 << i)) == 0) {
					currentControllers |= (1 << i);
					LimeLog.info("Assigned controller "+i);
					controllerNumber = i;
					return;
				}
			}
			
			controllerNumber = 0;
			LimeLog.info("No controller numbers left; using 0");
		}
		
		public void releaseControllerNumber() {
			LimeLog.info("Controller "+controllerNumber+" is now available");
			currentControllers &= ~(1 << controllerNumber);
		}
	}

	private NvConnection conn;
	private HashMap<Device, Gamepad> gamepads = new HashMap<Device, Gamepad>();
	private int currentControllers;

	public GamepadHandler(NvConnection conn) {
		this.conn = conn;
	}

	public GamepadHandler() {
		this(null);
	}
	
	private Gamepad getGamepad(Device dev, boolean create) {
		Gamepad gamepad = gamepads.get(dev);
		if (gamepad != null) {
			return gamepad;
		}
		else if (create) {
			gamepad = new Gamepad();
			gamepad.mapping = GamepadSettingsManager.getSettings();
			gamepad.assignControllerNumber();
			gamepads.put(dev, gamepad);
			return gamepad;
		}
		else {
			return null;
		}
	}

	public void setConnection(NvConnection conn) {
		this.conn = conn;
	}

	public void handleButton(Device device, int buttonId, boolean pressed) {
		Gamepad gamepad = getGamepad(device, true);
		Mapping mapped = gamepad.mapping.get(new SourceComponent(Type.BUTTON, buttonId, null));
		if (mapped == null) {
			//LimeLog.info("Unmapped button pressed: " + buttonId);
			return;
		}

		if (!mapped.padComp.isAnalog()) {
			handleDigitalComponent(gamepad, mapped, pressed);
		} else {
			handleAnalogComponent(gamepad, mapped.padComp, sanitizeValue(mapped, pressed));
		}

		//used for debugging
		//printInfo(device, new SourceComponent(Type.BUTTON, buttonId), mapped.padComp, pressed ? 1F : 0F);
	}

	public void handleAxis(Device device, int axisId, float newValue, float lastValue) {
		Direction mappedDir = null;
		if (newValue == 0) {
			if (lastValue > 0) {
				mappedDir = Direction.POSITIVE;
			} else {
				mappedDir = Direction.NEGATIVE;
			}
		} else {
			mappedDir = newValue > 0 ? Direction.POSITIVE : Direction.NEGATIVE;
		}
		
		Gamepad gamepad = getGamepad(device, true);
		Mapping mapped = gamepad.mapping.get(new SourceComponent(Type.AXIS, axisId, mappedDir));
		if (mapped == null) {
			//LimeLog.info("Unmapped axis moved: " + axisId);
			return;
		}
		float value = sanitizeValue(mapped, newValue);

		if (mapped.padComp.isAnalog()) {
			handleAnalogComponent(gamepad, mapped.padComp, value);
		} else {
			handleDigitalComponent(gamepad, mapped, (Math.abs(value) > 0.5));
		}

		//used for debugging
		//printInfo(device, new SourceComponent(Type.AXIS, axisId, mappedDir), mapped.padComp, newValue);
	}


	private static float sanitizeValue(Mapping mapped, boolean value) {
		if (mapped.invert) {
			return value ? 0F : 1F;
		} else {
			return value ? 1F : 0F;
		}
	}

	private static float sanitizeValue(Mapping mapped, float value) {
		float retVal = value;
		if (mapped.invert) {
			retVal = -retVal;
		}
		if (mapped.trigger) {
			retVal = (retVal + 1) / 2;
		}
		return retVal;
	}

	private void handleAnalogComponent(Gamepad gamepad, GamepadComponent padComp, float value) {
		switch (padComp) {
		case LS_RIGHT:
			gamepad.leftStickX = (short)(Math.abs(value) * 0x7FFE);
			break;
		case LS_LEFT:
			gamepad.leftStickX = (short)(-Math.abs(value) * 0x7FFE);
			break;
		case LS_UP:
			gamepad.leftStickY = (short)(Math.abs(value) * 0x7FFE);
			break;
		case LS_DOWN:
			gamepad.leftStickY = (short)(-Math.abs(value) * 0x7FFE);
			break;
		case RS_UP:
			gamepad.rightStickY = (short)(Math.abs(value) * 0x7FFE);
			break;
		case RS_DOWN:
			gamepad.rightStickY = (short)(-Math.abs(value) * 0x7FFE);
			break;
		case RS_RIGHT:
			gamepad.rightStickX = (short)(Math.abs(value) * 0x7FFE);
			break;
		case RS_LEFT:
			gamepad.rightStickX = (short)(-Math.abs(value) * 0x7FFE);
			break;
		case LT:
			// HACK: Fix polling so we don't have to do this
			if (Math.abs(value) < 0.9) {
				value = 0;
			}
			gamepad.leftTrigger = (byte)(Math.abs(value) * 0xFF);
			break;
		case RT:
			// HACK: Fix polling so we don't have to do this
			if (Math.abs(value) < 0.9) {
				value = 0;
			}
			gamepad.rightTrigger = (byte)(Math.abs(value) * 0xFF);
			break;
		default:
			LimeLog.warning("A mapping error has occured. Ignoring: " + padComp.name());
			break;
		}
		
		sendControllerPacket(gamepad);
	}

	private void handleDigitalComponent(Gamepad gamepad, Mapping mapped, boolean pressed) {
		switch (mapped.padComp) {
		case BTN_A:
			toggleButton(gamepad, ControllerPacket.A_FLAG, pressed);
			break;
		case BTN_X:
			toggleButton(gamepad, ControllerPacket.X_FLAG, pressed);
			break;
		case BTN_Y:
			toggleButton(gamepad, ControllerPacket.Y_FLAG, pressed);
			break;
		case BTN_B:
			toggleButton(gamepad, ControllerPacket.B_FLAG, pressed);
			break;
		case DPAD_UP:
			toggleButton(gamepad, ControllerPacket.UP_FLAG, pressed);
			break;
		case DPAD_DOWN:
			toggleButton(gamepad, ControllerPacket.DOWN_FLAG, pressed);
			break;
		case DPAD_LEFT:
			toggleButton(gamepad, ControllerPacket.LEFT_FLAG, pressed);
			break;
		case DPAD_RIGHT:
			toggleButton(gamepad, ControllerPacket.RIGHT_FLAG, pressed);
			break;
		case LS_THUMB:
			toggleButton(gamepad, ControllerPacket.LS_CLK_FLAG, pressed);
			break;
		case RS_THUMB:
			toggleButton(gamepad, ControllerPacket.RS_CLK_FLAG, pressed);
			break;
		case LB:
			toggleButton(gamepad, ControllerPacket.LB_FLAG, pressed);
			break;
		case RB:
			toggleButton(gamepad, ControllerPacket.RB_FLAG, pressed);
			break;
		case BTN_START:
			toggleButton(gamepad, ControllerPacket.PLAY_FLAG, pressed);
			break;
		case BTN_BACK:
			toggleButton(gamepad, ControllerPacket.BACK_FLAG, pressed);
			break;
		case BTN_SPECIAL:
			toggleButton(gamepad, ControllerPacket.SPECIAL_BUTTON_FLAG, pressed);
			break;
		default:
			LimeLog.warning("A mapping error has occured. Ignoring: " + mapped.padComp.name());
			return;
		}
		
		sendControllerPacket(gamepad);
	}

	/*
	 * Sends a controller packet to the specified connection containing the current gamepad values
	 */
	private void sendControllerPacket(Gamepad gamepad) {
		if (conn != null) {
			conn.sendControllerInput(gamepad.controllerNumber, gamepad.buttonFlags, gamepad.leftTrigger, gamepad.rightTrigger, 
					gamepad.leftStickX, gamepad.leftStickY, gamepad.rightStickX, gamepad.rightStickY);
		}
	}

	/*
	 * Prints out the specified event information for the given gamepad
	 * used for debugging, normally unused.
	 */
	@SuppressWarnings("unused")
	private static void printInfo(Device device, SourceComponent sourceComp, GamepadComponent padComp, float value) {

		StringBuilder builder = new StringBuilder();

		builder.append(sourceComp.getType().name() + ": ");
		builder.append(sourceComp.getId() + " ");
		builder.append("mapped to: " + padComp + " ");
		builder.append("changed to " + value);

		LimeLog.info(builder.toString());
	}

	/*
	 * Toggles a flag that indicates the specified button was pressed or released
	 */
	private static void toggleButton(Gamepad gamepad, short button, boolean pressed) {
		if (pressed) {
			gamepad.buttonFlags |= button;
		} else {
			gamepad.buttonFlags &= ~button;
		}
	}

	@Override
	public void handleDeviceAdded(Device device) {
		// Causes the creation of the gamepad object
		getGamepad(device, true);
	}

	@Override
	public void handleDeviceRemoved(Device device) {
		Gamepad gamepad = getGamepad(device, false);
		if (gamepad != null) {
			gamepad.releaseControllerNumber();
		}
	}

} public class GamepadListener implements NativeGamepadListener {
	private HashMap<Integer, Device> devices;
	private List<DeviceListener> listeners;
	
	private static GamepadListener singleton;
	
	public static GamepadListener getInstance() {
		if (singleton == null) {
			singleton = new GamepadListener();
		}
		return singleton;
	}
	
	private GamepadListener() {
		devices = new HashMap<Integer, Device>();
		listeners = new LinkedList<DeviceListener>();
	}
	
	public int deviceCount() {
		return devices.size();
	}
	
	public void addDeviceListener(DeviceListener listener) {
		listeners.add(listener);
	}
	
	public List<DeviceListener> getListeners() {
		return Collections.unmodifiableList(listeners);
	}
	
	public void removeListener(DeviceListener listener) {
		listeners.remove(listener);
	}
	
	public void deviceAttached(int deviceId, int numButtons, int numAxes) {
		Device dev = new Device(deviceId, numButtons, numAxes);
		
		devices.put(deviceId, dev);
		
		for (DeviceListener listener : listeners) {
			listener.handleDeviceAdded(dev);
		}
	}

	public void deviceRemoved(int deviceId) {
		Device dev = devices.get(deviceId);
		if (dev == null) {
			return;
		}
		
		for (DeviceListener listener : listeners) {
			listener.handleDeviceRemoved(dev);
		}
		
		devices.remove(deviceId);
	}

	public void buttonDown(int deviceId, int buttonId) {
		Device dev = devices.get(deviceId);
		for (DeviceListener listener : listeners) {
			listener.handleButton(dev, buttonId, true);
		}
	}

	public void buttonUp(int deviceId, int buttonId) {
		Device dev = devices.get(deviceId);
		for (DeviceListener listener : listeners) {
			listener.handleButton(dev, buttonId, false);
		}
	}

	public void axisMoved(int deviceId, int axisId, float value, float lastValue) {
		Device dev = devices.get(deviceId);
		for (DeviceListener listener : listeners) {
			listener.handleAxis(dev, axisId, value, lastValue);
		}
	}

} public class NativeGamepad {
	public static final int DEFAULT_DEVICE_POLLING_ITERATIONS = 100;
	public static final int DEFAULT_EVENT_POLLING_INTERVAL = 20;
	
	private static ArrayList<NativeGamepadListener> listenerList =
			new ArrayList<NativeGamepadListener>();
	private static boolean running = false;
	private static boolean initialized = false;
	private static Thread pollingThread = null;
	private static int devicePollingIterations = DEFAULT_DEVICE_POLLING_ITERATIONS;
	private static int pollingIntervalMs = DEFAULT_EVENT_POLLING_INTERVAL;

	static {
		System.loadLibrary("gamepad_jni");
	}
	
	private static native void init();
	private static native void shutdown();
	private static native int numDevices();
	private static native void detectDevices();
	private static native void processEvents();
	
	public static void addListener(NativeGamepadListener listener) {
		listenerList.add(listener);
	}
	
	public static void removeListener(NativeGamepadListener listener) {
		listenerList.remove(listener);
	}
	
	public static boolean isRunning() {
		return running;
	}
	
	public static void setDevicePollingIterations(int iterations) {
		devicePollingIterations = iterations;
	}
	
	public static int getDevicePollingIterations() {
		return devicePollingIterations;
	}
	
	public static void setPollingInterval(int interval) {
		pollingIntervalMs = interval;
	}
	
	public static int getPollingInterval() {
		return pollingIntervalMs;
	}
	
	public static void start() {
		if (!running) {
			running = true;
			startPolling();
		}
	}
	
	public static void stop() {
		if (running) {
			stopPolling();
			running = false;
		}
	}
	
	public static void release() {
		if (running) {
			throw new IllegalStateException("Cannot release running NativeGamepad");
		}
		
		if (initialized) {
			NativeGamepad.shutdown();
			initialized = false;
		}
	}
	
	public static int getDeviceCount() {
		if (!running) {
			throw new IllegalStateException("NativeGamepad not running");
		}
		
		return NativeGamepad.numDevices();
	}
	
	private static void startPolling() {
		pollingThread = new Thread() {
			@Override
			public void run() {
				int iterations = 0;
				
				if (!initialized) {
					NativeGamepad.init();
					initialized = true;
				}
				
				while (!isInterrupted()) {
					// If we have no devices, we don't bother with the event
					// polling interval. We just run the device polling interval.
					if (getDeviceCount() == 0) {
						NativeGamepad.detectDevices();
						NativeGamepad.processEvents();
						
						try {
							Thread.sleep(pollingIntervalMs * devicePollingIterations);
						} catch (InterruptedException e) {
							return;
						}
					}
					else {
						if ((iterations++ % devicePollingIterations) == 0) {
							NativeGamepad.detectDevices();
						}
						
						NativeGamepad.processEvents();
						
						try {
							Thread.sleep(pollingIntervalMs);
						} catch (InterruptedException e) {
							return;
						}
					}
				}
			}
		};
		pollingThread.setName("Native Gamepad - Polling Thread");
		pollingThread.start();
	}
	
	private static void stopPolling() {
		if (pollingThread != null) {
			pollingThread.interrupt();
			
			try {
				pollingThread.join();
			} catch (InterruptedException e) {}
		}
	}
	
	public static void deviceAttachCallback(int deviceId, int numButtons, int numAxes) {
		LimeLog.info(deviceId + " has attached.");
		for (NativeGamepadListener listener : listenerList) {
			listener.deviceAttached(deviceId, numButtons, numAxes);
		}
	}
	
	public static void deviceRemoveCallback(int deviceId) {
		LimeLog.info(deviceId + " has detached.");
		for (NativeGamepadListener listener : listenerList) {
			listener.deviceRemoved(deviceId);
		}
	}
	
	public static void buttonUpCallback(int deviceId, int buttonId) {
		for (NativeGamepadListener listener : listenerList) {
			listener.buttonUp(deviceId, buttonId);
		}
	}
	
	public static void buttonDownCallback(int deviceId, int buttonId) {
		for (NativeGamepadListener listener : listenerList) {
			listener.buttonDown(deviceId, buttonId);
		}
	}
	
	public static void axisMovedCallback(int deviceId, int axisId, float value, float lastValue) {
		for (NativeGamepadListener listener : listenerList) {
			listener.axisMoved(deviceId, axisId, value, lastValue);
		}
	}
} public abstract class PreferencesManager {
	private static Preferences cachedPreferences = null;

	/**
	 * Writes the specified preferences to the preferences file and updates the cached preferences.
	 * @param prefs the preferences to be written out
	 */
	public static void writePreferences(Preferences prefs) {
		LimeLog.info("Writing Preferences");
		File prefFile = SettingsManager.getInstance().getSettingsFile();

		SettingsManager.writeSettings(prefFile, prefs);
		cachedPreferences = prefs;
	}

	/**
	 * Checks if the preferences file exists
	 * @return true if preferences exist
	 */
	public static boolean hasExistingPreferences() {
		File prefFile = SettingsManager.getInstance().getSettingsFile();
		return SettingsManager.readSettings(prefFile, Preferences.class) != null;
	}

	/**
	 * Reads the user preferences from the preferences file and caches them
	 * @return the user preferences
	 */
	public static Preferences getPreferences() {
		if (cachedPreferences == null) {
			LimeLog.info("Reading Preferences");
			File prefFile = SettingsManager.getInstance().getSettingsFile();
			Preferences savedPref = (Preferences)SettingsManager.readSettings(prefFile, Preferences.class);
			cachedPreferences = savedPref;
		}
		if (cachedPreferences == null) {
			LimeLog.warning("Unable to get preferences, using default");
			cachedPreferences = new Preferences();
			writePreferences(cachedPreferences);
		}
		return cachedPreferences;
	}

	/**
	 * Represents a user's preferences
	 * @author Diego Waxemberg
	 */
	public static class Preferences implements Serializable {
		private static final long serialVersionUID = -5575445156207845705L;

		/**
		 * The possible resolutions for the stream
		 */
		public enum Resolution { RES_720_30(1280, 720, 30, 5), RES_720_60(1280, 720, 60, 10),
			RES_1080_30(1920, 1080, 30, 10), RES_1080_60(1920, 1080, 60, 20);
			public int width;
			public int height;
			public int frameRate;
			public int defaultBitrate;
			
			/**
			 * Creates a new resolution with the specified name
			 */
			private Resolution(int width, int height, int frameRate, int defaultBitrate) {
				this.width = width;
				this.height = height;
				this.frameRate = frameRate;
				this.defaultBitrate = defaultBitrate;
			}

			/**
			 * Gets the specified name for this resolution
			 * @return the specified name of this resolution
			 */
			@Override
			public String toString() {
				return String.format("%dx%d (%dHz)", width, height, frameRate);
			}
			
			public static Resolution findRes(int height, int refresh) {
			    for (Resolution res : Resolution.values()) {
			        if (res.height == height && res.frameRate == refresh) {
			            return res;
			        }
			    }
			    return null;
			}
		};

		private Resolution res;
		private int bitrate;
		private boolean fullscreen;
		private String host;
		private String uniqueId;
		private boolean localAudio;
		private boolean allowResolutionChange;
		private boolean keepAspectRatio;

		/**
		 * constructs default preferences: 720p 60Hz
		 * full-screen will be default for Windows (where it always runs properly)
		 * windowed will be default for other platforms
		 */
		public Preferences() {
			this(Resolution.RES_720_60,
                 System.getProperty("os.name", "").contains("Windows"));
		}

		/**
		 * Constructs a preference with the specified values
		 * @param res the <code>Resolution</code> to use
		 * @param fullscreen whether to start the stream in fullscreen
		 */
		private Preferences(Resolution res, boolean fullscreen) {
			this.res = res;
			this.bitrate = res.defaultBitrate;
			this.fullscreen = fullscreen;
			this.host = "GeForce PC host";
			this.uniqueId = String.format("%016x", new Random().nextLong());
			this.localAudio = false;
			this.allowResolutionChange = true;
			this.keepAspectRatio = true;
		}

		/**
		 * The saved host in this preference
		 * @return the last used host
		 */
		public String getHost() {
			return host;
		}

		/**
		 * Sets the host for this preference
		 * @param host the host to save
		 */
		public void setHost(String host) {
			this.host = host;
		}

		/**
		 * Gets the resolution in this preference
		 * @return the stored resolution
		 */
		public Resolution getResolution() {
			// We removed some resolution values, so fixup the resolution
			// if the enum value couldn't be found
			if (res == null) {
				res = Resolution.RES_720_60;
			}
			return res;
		}

		/**
		 * Gets the bitrate in this preference
		 * @return the stored bitrate
		 */
		public int getBitrate() {
			return bitrate;
		}
		
		/**
		 * Gets whether to use fullscreen
		 * @return the stored fullscreen mode
		 */
		public boolean getFullscreen() {
			return fullscreen;
		}
		
		/**
		 * Gets whether to use local audio
		 * @return the stored localAudio
		 */
		public boolean getLocalAudio() {
			return localAudio;
		}

		/**
		 * Sets the resolution in this preference
		 * @param res the resolution to save
		 */
		public void setResolution(Resolution res) {
			this.res = res;
		}

		/**
		 * Sets the bitrate in this preference
		 * @param bitrate the bitrate to save
		 */
		public void setBitrate(int bitrate) {
			this.bitrate = bitrate;
		}
		
		/**
		 * Sets the fullscreen mode of this preference
		 * @param fullscreen whether to use fullscreen
		 */
		public void setFullscreen(boolean fullscreen) {
			this.fullscreen = fullscreen;
		}
		
		/**
		 * Gets the unique ID
		 * @return uniqueId the unique ID
		 */
		public String getUniqueId() {
			return uniqueId;
		}
		
		/**
		 * Sets the local audio use of this preference
		 * @param localAudio whether to use localAudio
		 */
		public void setLocalAudio(boolean localAudio) {
			this.localAudio = localAudio;
		}

		
		public boolean getAllowResolutionChange() {
			return allowResolutionChange;
		}
		
		public void setAllowResolutionChange(boolean allowResolutionChange) {
			this.allowResolutionChange = allowResolutionChange;
		}

		public boolean isKeepAspectRatio() {
			return keepAspectRatio;
		}

		public void setKeepAspectRatio(boolean keepAspectRatio) {
			this.keepAspectRatio = keepAspectRatio;
		}
		
		
	}
} public class SettingsManager {
	/**
	 * Directory to which settings will be saved
	 */
	public static String SETTINGS_DIR = System.getProperty("user.home") + File.separator + "Limelight";
	
	//directory to hold limelight settings
	private File settingsDir;
	
	private File settingsFile;
	private File gamepadFile;
	
	private static SettingsManager manager;
	
	/*
	 * Constructs a manager that initializes the settings files
	 */
	private SettingsManager() {
		settingsFile = new File(SETTINGS_DIR + File.separator + "settings.json");
		gamepadFile = new File(SETTINGS_DIR + File.separator + "gamepad.json");
		settingsDir = new File(SETTINGS_DIR);
	}
	
	/**
	 * Gets an instance of the manager, this is a singleton
	 * @return the instance of the manager
	 */
	public static SettingsManager getInstance() {
		if (manager == null) {
			manager = new SettingsManager();
		}
		return manager;
	}
	
	/**
	 * Gets the gamepad preference file, if the file does not exist, it is created first
	 * @return the gamepad preference file
	 */
	public File getGamepadFile() {
		if (!settingsDir.exists()) {
			settingsDir.mkdirs();
		}
		
		if (!gamepadFile.exists()) {
			try {
				gamepadFile.createNewFile();
			} catch (IOException e) {
				LimeLog.warning("Unable to create gamepad file");
				return null;
			}
		}
		
		return gamepadFile;
	}
	
	/**
	 * Gets the settings file (user preferences), if the file does not exist, it is created first
	 * @return the settings file
	 */
	public File getSettingsFile() {
		if (!settingsDir.exists()) {
			settingsDir.mkdirs();
		}
		
		if (!settingsFile.exists()) {
			try {
				settingsFile.createNewFile();
			} catch (IOException e) {
				LimeLog.warning("Unable to create setting file");
				return null;
			}
		}
		
		return settingsFile;
	}
	
	/**
	 * Reads the specified file as a settings file and returns the result.
	 * <br>A settings file must be a java serialized object
	 * @param <T>
	 * @param file the file to read in
	 * @return the settings represented in this file
	 */
	public static <T> Object readSettings(File file, Class<T> klass) {
		Gson gson = new GsonBuilder()
          .setPrettyPrinting().enableComplexMapKeySerialization().create();
		T settings = null;
		BufferedReader br = null;
		try {
			br = new BufferedReader(new FileReader(file));
			settings = gson.fromJson(br, klass);
		} catch (FileNotFoundException e) {
			LimeLog.warning("Could not find " + file.getName() + " settings file");
			e.printStackTrace();
		} catch (JsonSyntaxException e) {
			LimeLog.warning("JSON settings are corrupt; returning null");
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					LimeLog.warning("Could not close gamepad settings file");
					e.printStackTrace();
				}
			}
		}
		return settings;
	}
	
	/**
	 * Writes the specified settings to the desired file
	 * @param file the file to write the settings to 
	 * @param settings the settings to be written out
	 */
	public static <T extends Serializable> void writeSettings(File file, T settings) {
		Gson gson = new GsonBuilder()
			.setPrettyPrinting().enableComplexMapKeySerialization().create();
		FileWriter writer = null;

		try {
			String json = gson.toJson(settings);
			writer = new FileWriter(file);
			writer.write(json);
		} catch (FileNotFoundException e) {
			LimeLog.warning("Could not find " + file.getName() + " settings file");
			e.printStackTrace();

		} catch (IOException e) {
			LimeLog.warning("Could not write to " + file.getName() + " settings file");
			e.printStackTrace();

		} finally {
			if (writer != null) {
				try {
					writer.close();
				} catch (IOException e) {
					LimeLog.warning("Unable to close " + file.getName() + " settings file");
					e.printStackTrace();
				}
			}
		}
	}
} public static class Preferences implements Serializable {
	private static final long serialVersionUID = -5575445156207845705L;

	/**
	 * The possible resolutions for the stream
	 */
	public enum Resolution { RES_720_30(1280, 720, 30, 5), RES_720_60(1280, 720, 60, 10),
		RES_1080_30(1920, 1080, 30, 10), RES_1080_60(1920, 1080, 60, 20);
		public int width;
		public int height;
		public int frameRate;
		public int defaultBitrate;
			
		/**
		 * Creates a new resolution with the specified name
		 */
		private Resolution(int width, int height, int frameRate, int defaultBitrate) {
			this.width = width;
			this.height = height;
			this.frameRate = frameRate;
			this.defaultBitrate = defaultBitrate;
		}

		/**
		 * Gets the specified name for this resolution
		 * @return the specified name of this resolution
		 */
		@Override
		public String toString() {
			return String.format("%dx%d (%dHz)", width, height, frameRate);
		}
			
		public static Resolution findRes(int height, int refresh) {
		    for (Resolution res : Resolution.values()) {
		        if (res.height == height && res.frameRate == refresh) {
		            return res;
		        }
		    }
		    return null;
		}
	};

	private Resolution res;
	private int bitrate;
	private boolean fullscreen;
	private String host;
	private String uniqueId;
	private boolean localAudio;
	private boolean allowResolutionChange;
	private boolean keepAspectRatio;

	/**
	 * constructs default preferences: 720p 60Hz
	 * full-screen will be default for Windows (where it always runs properly)
	 * windowed will be default for other platforms
	 */
	public Preferences() {
		this(Resolution.RES_720_60,
                System.getProperty("os.name", "").contains("Windows"));
	}

	/**
	 * Constructs a preference with the specified values
	 * @param res the <code>Resolution</code> to use
	 * @param fullscreen whether to start the stream in fullscreen
	 */
	private Preferences(Resolution res, boolean fullscreen) {
		this.res = res;
		this.bitrate = res.defaultBitrate;
		this.fullscreen = fullscreen;
		this.host = "GeForce PC host";
		this.uniqueId = String.format("%016x", new Random().nextLong());
		this.localAudio = false;
		this.allowResolutionChange = true;
		this.keepAspectRatio = true;
	}

	/**
	 * The saved host in this preference
	 * @return the last used host
	 */
	public String getHost() {
		return host;
	}

	/**
	 * Sets the host for this preference
	 * @param host the host to save
	 */
	public void setHost(String host) {
		this.host = host;
	}

	/**
	 * Gets the resolution in this preference
	 * @return the stored resolution
	 */
	public Resolution getResolution() {
		// We removed some resolution values, so fixup the resolution
		// if the enum value couldn't be found
		if (res == null) {
			res = Resolution.RES_720_60;
		}
		return res;
	}

	/**
	 * Gets the bitrate in this preference
	 * @return the stored bitrate
	 */
	public int getBitrate() {
		return bitrate;
	}
		
	/**
	 * Gets whether to use fullscreen
	 * @return the stored fullscreen mode
	 */
	public boolean getFullscreen() {
		return fullscreen;
	}
		
	/**
	 * Gets whether to use local audio
	 * @return the stored localAudio
	 */
	public boolean getLocalAudio() {
		return localAudio;
	}

	/**
	 * Sets the resolution in this preference
	 * @param res the resolution to save
	 */
	public void setResolution(Resolution res) {
		this.res = res;
	}

	/**
	 * Sets the bitrate in this preference
	 * @param bitrate the bitrate to save
	 */
	public void setBitrate(int bitrate) {
		this.bitrate = bitrate;
	}
		
	/**
	 * Sets the fullscreen mode of this preference
	 * @param fullscreen whether to use fullscreen
	 */
	public void setFullscreen(boolean fullscreen) {
		this.fullscreen = fullscreen;
	}
		
	/**
	 * Gets the unique ID
	 * @return uniqueId the unique ID
	 */
	public String getUniqueId() {
		return uniqueId;
	}
		
	/**
	 * Sets the local audio use of this preference
	 * @param localAudio whether to use localAudio
	 */
	public void setLocalAudio(boolean localAudio) {
		this.localAudio = localAudio;
	}

		
	public boolean getAllowResolutionChange() {
		return allowResolutionChange;
	}
		
	public void setAllowResolutionChange(boolean allowResolutionChange) {
		this.allowResolutionChange = allowResolutionChange;
	}

	public boolean isKeepAspectRatio() {
		return keepAspectRatio;
	}

	public void setKeepAspectRatio(boolean keepAspectRatio) {
		this.keepAspectRatio = keepAspectRatio;
	}
		
		
} public enum Resolution { RES_720_30(1280, 720, 30, 5), RES_720_60(1280, 720, 60, 10),
	RES_1080_30(1920, 1080, 30, 10), RES_1080_60(1920, 1080, 60, 20);
	public int width;
	public int height;
	public int frameRate;
	public int defaultBitrate;
			
	/**
	 * Creates a new resolution with the specified name
	 */
	private Resolution(int width, int height, int frameRate, int defaultBitrate) {
		this.width = width;
		this.height = height;
		this.frameRate = frameRate;
		this.defaultBitrate = defaultBitrate;
	}

	/**
	 * Gets the specified name for this resolution
	 * @return the specified name of this resolution
	 */
	@Override
	public String toString() {
		return String.format("%dx%d (%dHz)", width, height, frameRate);
	}
			
	public static Resolution findRes(int height, int refresh) {
	    for (Resolution res : Resolution.values()) {
	        if (res.height == height && res.frameRate == refresh) {
	            return res;
	        }
	    }
	    return null;
	}
};
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import com.limelight.binding.LibraryHelper;
import com.limelight.binding.PlatformBinding;
import com.limelight.gui.MainFrame;
import com.limelight.gui.StreamFrame;
import com.limelight.input.gamepad.GamepadHandler;
import com.limelight.input.gamepad.GamepadListener;
import com.limelight.input.gamepad.NativeGamepad;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.settings.PreferencesManager;
import com.limelight.settings.SettingsManager;
import com.limelight.settings.PreferencesManager.Preferences;
import com.limelight.settings.PreferencesManager.Preferences.Resolution;
package com.limelight;




/**
 * Main class for Limelight-pc contains methods for starting the application as well
 * as the stream to the host pc.
 * @author Diego Waxemberg<br>
 * Cameron Gutman
 */
public class Limelight implements NvConnectionListener {
	public static final double VERSION = 1.0;
	public static boolean COMMAND_LINE_LAUNCH = false;

	private String host;
	private StreamFrame streamFrame;
	private NvConnection conn;
	private boolean connectionTerminating;
	private static JFrame limeFrame;
	private GamepadHandler gamepad;