/*
 * SwingInstall.java
 *
 * Originally written by Slava Pestov for the jEdit installer project. This work
 * has been placed into the public domain. You may use this work in any way and
 * for any purpose you wish.
 *
 * THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND, NOT EVEN THE
 * IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE, ASSUMES
 * _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE RESULTING FROM THE USE, MODIFICATION,
 * OR REDISTRIBUTION OF THIS SOFTWARE.
 */

package installer;

import javax.swing.border.*;
import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.awt.event.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.*;

/*
 * Graphical front-end to installer.
 */
public class SwingInstall extends JFrame
{
	public SwingInstall()
	{
		installer = new Install();
		osTasks = OperatingSystem.getOperatingSystem().getOSTasks(installer);

		appName = installer.getProperty("app.name");
		appVersion = installer.getProperty("app.version");

		setTitle(appName + " " + appVersion + " installer");

		JPanel content = new JPanel(new WizardLayout());
		setContentPane(content);

		caption = new JLabel();
		caption.setFont(new Font("SansSerif",Font.BOLD,18));

		ActionHandler actionHandler = new ActionHandler();

		cancelButton = new JButton("Cancel");
		cancelButton.setRequestFocusEnabled(false);
		cancelButton.addActionListener(actionHandler);
		prevButton = new JButton("Previous");
		prevButton.setRequestFocusEnabled(false);
		prevButton.addActionListener(actionHandler);
		nextButton = new JButton();
		nextButton.setRequestFocusEnabled(false);
		nextButton.addActionListener(actionHandler);

		content.add(caption);
		content.add(cancelButton);
		content.add(prevButton);
		content.add(nextButton);

		String clazz = OperatingSystem.getOperatingSystem()
				.getClass().getName();
		String completedInfo = "done-" + clazz.substring(
			clazz.indexOf('$') + 1) + ".html";

		pages = new Component[] {
			new TextPanel(installer.getProperty("app.readme")),
			new TextPanel(installer.getProperty("app.license")),
			chooseDirectory = new ChooseDirectory(),
			selectComponents = new SelectComponents(),
			progress = new SwingProgress(),
			new TextPanel(completedInfo)
		};

		for(int i = 0; i < pages.length; i++)
			content.add(pages[i]);

		pageChanged();

		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		addWindowListener(new WindowHandler());

		Dimension screen = getToolkit().getScreenSize();
		pack();
		setLocation((screen.width - getSize().width) / 2,
			(screen.height - getSize().height) / 2);
		setVisible(true);
	}

	// package-private members
	// package-private, not private, for speedy access by inner classes
	Install installer;
	OperatingSystem.OSTask[] osTasks;
	String appName;
	String appVersion;

	JLabel caption;

	ChooseDirectory chooseDirectory;
	SelectComponents selectComponents;
	SwingProgress progress;

	JButton cancelButton;
	JButton prevButton;
	JButton nextButton;
	Component[] pages;
	int currentPage;

	private static final int PADDING = 12;

	void install()
	{
		Vector components = new Vector();
		int size = 0;

		JPanel comp = selectComponents.comp;
		Vector ids = selectComponents.filesets;

		for(int i = 0; i < comp.getComponentCount(); i++)
		{
			if(((JCheckBox)comp.getComponent(i))
				.getModel().isSelected())
			{
				size += installer.getIntegerProperty(
					"comp." + ids.elementAt(i) + ".real-size");
				components.addElement(installer.getProperty(
					"comp." + ids.elementAt(i) + ".fileset"));
			}
		}

		String installDir = chooseDirectory.installDir.getText();

		Map osTaskDirs = chooseDirectory.osTaskDirs;
		Iterator keys = osTaskDirs.keySet().iterator();
		while(keys.hasNext())
		{
			OperatingSystem.OSTask osTask = (OperatingSystem.OSTask)keys.next();
			String dir = ((JTextField)osTaskDirs.get(osTask)).getText();
			if(dir != null && dir.trim().length() != 0)
			{
				osTask.setEnabled(true);
				osTask.setDirectory(dir);
			}
			else
				osTask.setEnabled(false);
		}

		InstallThread thread = new InstallThread(
			installer,progress,
			installDir,osTasks,
			size,components);
		progress.setThread(thread);
		thread.start();
	}

	private void pageChanged()
	{
		switch(currentPage)
		{
		case 0:
			caption.setText("Installing " + appName);

			nextButton.setText("Next");
			prevButton.setEnabled(false);
			nextButton.setEnabled(true);
			break;
		case 1:
			caption.setText(installer.getProperty("app.license.title"));

			nextButton.setText("Next");
			prevButton.setEnabled(true);
			nextButton.setEnabled(true);
			break;
		case 2:
			caption.setText("Specify where " + appName
				+ " is to be installed");

			nextButton.setText("Next");
			prevButton.setEnabled(true);
			if(!chooseDirectory.isOK())nextButton.setEnabled(false);
			break;
		case 3:
			caption.setText("Choose components to install");

			nextButton.setText("Install");
			prevButton.setEnabled(true);
			nextButton.setEnabled(true);
			break;
		case 4:
			caption.setText("Installing " + appName);

			nextButton.setText("Finish");
			prevButton.setEnabled(false);
			nextButton.setEnabled(false);
			install();
			break;
		case 5:
			caption.setText("Installation complete");

			nextButton.setText("Finish");
			prevButton.setEnabled(false);
			nextButton.setEnabled(true);
			cancelButton.setEnabled(false);
			break;
		}

		getRootPane().invalidate();
		getRootPane().validate();
	}

	class ActionHandler implements ActionListener
	{
		public void actionPerformed(ActionEvent evt)
		{
			Object source = evt.getSource();
			if(source == cancelButton)
				System.exit(0);
			else if(source == prevButton)
			{
				currentPage--;
				pageChanged();
			}
			else if(source == nextButton)
			{
				if(currentPage == pages.length - 1)
					System.exit(0);
				else
				{
					currentPage++;
					pageChanged();
				}
			}
		}
	}

	class WindowHandler extends WindowAdapter
	{
		public void windowClosing(WindowEvent evt)
		{
			System.exit(0);
		}
	}

	class WizardLayout implements LayoutManager
	{
		public void addLayoutComponent(String name, Component comp)
		{
		}

		public void removeLayoutComponent(Component comp)
		{
		}

		public Dimension preferredLayoutSize(Container parent)
		{
			Dimension dim = new Dimension();

			Dimension captionSize = caption.getPreferredSize();
			dim.width = captionSize.width;

			for(int i = 0; i < pages.length; i++)
			{
				Dimension _dim = pages[i].getPreferredSize();
				dim.width = Math.max(_dim.width,dim.width);
				dim.height = Math.max(_dim.height,dim.height);
			}

			dim.width += PADDING * 2;
			dim.height += PADDING * 2;
			dim.height += nextButton.getPreferredSize().height;
			dim.height += captionSize.height;
			return dim;
		}

		public Dimension minimumLayoutSize(Container parent)
		{
			return preferredLayoutSize(parent);
		}

		public void layoutContainer(Container parent)
		{
			Dimension size = parent.getSize();

			Dimension captionSize = caption.getPreferredSize();
			caption.setBounds(PADDING,PADDING,captionSize.width,
				captionSize.height);

			// make all buttons the same size
			Dimension buttonSize = cancelButton.getPreferredSize();
			buttonSize.width = Math.max(buttonSize.width,prevButton.getPreferredSize().width);
			buttonSize.width = Math.max(buttonSize.width,nextButton.getPreferredSize().width);

			// cancel button goes on far left
			cancelButton.setBounds(
				PADDING,
				size.height - buttonSize.height - PADDING,
				buttonSize.width,
				buttonSize.height);

			// prev and next buttons are on the right
			prevButton.setBounds(
				size.width - buttonSize.width * 2 - 6 - PADDING,
				size.height - buttonSize.height - PADDING,
				buttonSize.width,
				buttonSize.height);

			nextButton.setBounds(
				size.width - buttonSize.width - PADDING,
				size.height - buttonSize.height - PADDING,
				buttonSize.width,
				buttonSize.height);

			// calculate size for current page
			Rectangle currentPageBounds = new Rectangle();
			currentPageBounds.x = PADDING;
			currentPageBounds.y = PADDING * 2 + captionSize.height;
			currentPageBounds.width = size.width - currentPageBounds.x
				- PADDING;
			currentPageBounds.height = size.height - buttonSize.height
				- currentPageBounds.y - PADDING * 2;

			for(int i = 0; i < pages.length; i++)
			{
				Component page = pages[i];
				page.setBounds(currentPageBounds);
				page.setVisible(i == currentPage);
			}
		}
	}

	class TextPanel extends JPanel
	{
		TextPanel(String file)
		{
			super(new BorderLayout());

			JEditorPane text = new JEditorPane();

			try
			{
				text.setPage(TextPanel.this.getClass().getResource(file));
			}
			catch(Exception e)
			{
				text.setText("Error loading '" + file + "'");
				e.printStackTrace();
			}

			text.setEditable(false);

			JScrollPane scrollPane = new JScrollPane(text);
			Dimension dim = new Dimension();
			dim.width = 450;
			dim.height = 200;
			scrollPane.setPreferredSize(dim);
			TextPanel.this.add(BorderLayout.CENTER,scrollPane);
		}
	}

	class DirVerifier extends InputVerifier
	{
		private JTextComponent message;
		private Object pos;
		private JComponent parent;
		
		public DirVerifier(JComponent parent, Object pos)
		{
			super();
			message = new JTextArea(" ");
			message.setEditable(false);
			message.setBackground(parent.getBackground());
			this.parent = parent;
			this.pos = pos;
		}
		
		public boolean shouldYieldFocus(JComponent input)
		{
			return verify(input);
		}
		
		public boolean verify(JComponent input)
		{
			if(input instanceof JTextComponent)
			{
				String dir = ((JTextComponent)input).getText();
				if(checkNull(dir) &&
					checkExistNotDirectory(dir) &&
					checkExistNotEmpty(dir) &&
					checkRelative(dir))
				{
					//everything is perfect, clean label
					if(message.getParent()!=null)
					{
						SwingUtilities.invokeLater(new Runnable()
							{
								public void run()
								{
									parent.remove(message);
									parent.revalidate();
									parent.repaint();
								}
							});
					}
				}
				else
				{
					if(message.getParent()==null)
					{
						SwingUtilities.invokeLater(new Runnable()
							{
								public void run()
								{
									parent.add(message,pos);
									parent.revalidate();
									parent.repaint();
								}
							});
					}
					else message.repaint();
				}
			}
			return true;
		}
		
		private boolean checkNull(String file)
		{
			if(file.trim().length()==0)
			{
				message.setForeground(Color.red);
				message.setText(installer.getProperty("dir.null"));
				return false;
			}
			else return true;
		}
		
		private boolean checkRelative(String dir)
		{
			File f = new File(dir);
			if(!f.isAbsolute())
			{
				String msg = installer.getProperty("dir.relative");
				try
				{
					String full = f.getCanonicalPath();
					message.setForeground(Color.orange);
					message.setText(msg+'\n'+full);
				}
				catch(IOException ioe)
				{
					message.setForeground(Color.red);
					msg = installer.getProperty("dir.cant-resolve");
					message.setText(msg);
				}
				return false;
			}
			else return true;
		}
		
		private boolean checkExistNotDirectory(String dir)
		{
			File f = new File(dir);
			if(f.exists() && !f.isDirectory())
			{
				message.setForeground(Color.red);
				message.setText(installer.getProperty("dir.not-directory"));
				return false;
			}
			else return true;
		}
		
		private boolean checkExistNotEmpty(String dir)
		{
			File f = new File(dir);
			String[]cnt = f.list();
			if(cnt!=null && cnt.length>0)
			{
				message.setForeground(Color.orange);
				message.setText(installer.getProperty("dir.not-empty"));
				return false;
			}
			else return true;
		}
	}
	/**
	 * only install directory is required, as it's necessary to scripts, etc.
	 * If one uses the installer only for OS tasks, then one deselects all components
	 * and that's all.
	 */
	class ChooseDirectory extends JPanel
	{
		JTextField installDir;
		Map osTaskDirs;

		ChooseDirectory()
		{
			super(new BorderLayout());
			osTaskDirs = new HashMap();

			
			JPanel directoryPanel = new JPanel(new GridBagLayout());

			installDir = addField(directoryPanel,"Install program in:",
				OperatingSystem.getOperatingSystem()
				.getInstallDirectory(appName,appVersion));

			installDir.addFocusListener(new FocusAdapter()
				{
					public void focusLost(FocusEvent fe)
					{
						nextButton.setEnabled(isOK());
					}
				});
			for(int i = 0; i < osTasks.length; i++)
			{
				OperatingSystem.OSTask osTask = osTasks[i];
				String label = osTask.getLabel();
				if(label != null)
				{
					JTextField field = addField(directoryPanel,label,
						osTask.getDirectory());
					osTaskDirs.put(osTask,field);
				}
			}
			ChooseDirectory.this.add(BorderLayout.NORTH,directoryPanel);
		}

		boolean isOK()
		{
			if(installDir.getText().length()==0)return false;
			File f = new File(installDir.getText());
			return !(f.exists()&&!f.isDirectory());
		}

		private GridBagConstraints c = new GridBagConstraints();
		private JTextField addField(JPanel directoryPanel, String label,
			String defaultText)
		{
			
			//new line
			c.gridy++;
			
			//message
			JTextField field = new JTextField(defaultText);
			c.insets.bottom=3;
			c.gridx=0;
			c.gridwidth=3;
			c.insets.left=0;
			c.insets.right=0;
			c.anchor=GridBagConstraints.LINE_START;
			DirVerifier verif = new DirVerifier(directoryPanel,c.clone());
			
			
			field.setInputVerifier(verif);
			
			c.insets.bottom=12;
			c.gridx=0;
			c.gridy++;
			c.gridwidth=1;
			c.anchor=GridBagConstraints.LINE_END;
			directoryPanel.add(new JLabel(label,SwingConstants.RIGHT),c);

			c.gridx=1;
			c.fill=GridBagConstraints.HORIZONTAL;
			c.anchor=GridBagConstraints.CENTER;
			c.insets.left=12;
			c.insets.right=12;
			c.weightx=1.0;
			directoryPanel.add(field,c);
			
			
			JButton choose = new JButton("Choose...");
			choose.setRequestFocusEnabled(false);
			choose.addActionListener(new ActionHandler(field));
			c.gridx=2;
			c.insets.left=0;
			c.insets.right=0;
			c.fill=GridBagConstraints.NONE;
			c.weightx=0;
			directoryPanel.add(choose,c);

			return field;
		}

		class ActionHandler implements ActionListener
		{
			JTextField field;

			ActionHandler(JTextField field)
			{
				this.field = field;
			}

			public void actionPerformed(ActionEvent evt)
			{
				File directory = new File(field.getText());
				JFileChooser chooser = new JFileChooser(directory.getParent());
				chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
				chooser.setSelectedFile(directory);

				if(chooser.showOpenDialog(SwingInstall.this)
					== JFileChooser.APPROVE_OPTION)
				{
						field.setText(chooser.getSelectedFile().getPath());
						field.getInputVerifier().verify(field);
				}
			}
		}
	}

	class SelectComponents extends JPanel
	implements ActionListener
	{
		JPanel comp;
		JLabel sizeLabel;
		Vector filesets;

		SelectComponents()
		{
			super(new BorderLayout());

			comp = createCompPanel();
			SelectComponents.this.add(BorderLayout.NORTH,comp);

			sizeLabel = new JLabel("",SwingConstants.LEFT);
			SelectComponents.this.add(BorderLayout.SOUTH,sizeLabel);

			updateSize();
		}

		public void actionPerformed(ActionEvent evt)
		{
			updateSize();
		}

		private JPanel createCompPanel()
		{
			filesets = new Vector();

			int count = installer.getIntegerProperty("comp.count");
			JPanel panel = new JPanel(new GridLayout(count,1));

			String osClass = OperatingSystem.getOperatingSystem()
				.getClass().getName();
			osClass = osClass.substring(osClass.indexOf('$') + 1);

			for(int i = 0; i < count; i++)
			{
				String os = installer.getProperty("comp." + i + ".os");

				if(os != null && !osClass.equals(os))
					continue;

				JCheckBox checkBox = new JCheckBox(
					installer.getProperty("comp." + i + ".name")
					+ " (" + installer.getProperty("comp." + i
					+ ".disk-size") + "Kb)");
				checkBox.getModel().setSelected(true);
				checkBox.addActionListener(this);
				checkBox.setRequestFocusEnabled(false);

				filesets.addElement(new Integer(i));

				panel.add(checkBox);
			}

			Dimension dim = panel.getPreferredSize();
			dim.width = Integer.MAX_VALUE;
			panel.setMaximumSize(dim);

			return panel;
		}

		private void updateSize()
		{
			int size = 0;

			for(int i = 0; i < filesets.size(); i++)
			{
				if(((JCheckBox)comp.getComponent(i))
					.getModel().isSelected())
				{
					size += installer.getIntegerProperty("comp."
						+ filesets.elementAt(i)
						+ ".disk-size");
				}
			}

			sizeLabel.setText("Estimated disk usage of selected"
				+ " components: " + size + "Kb");
		}
	}

	class SwingProgress extends JPanel implements Progress
	{
		JProgressBar progress;
		InstallThread thread;

		SwingProgress()
		{
			super(new BorderLayout());

			progress = new JProgressBar();
			progress.setStringPainted(true);

			SwingProgress.this.add(BorderLayout.NORTH,progress);
		}

		public void setMaximum(final int max)
		{
			SwingUtilities.invokeLater(new Runnable()
			{
				public void run()
				{
					progress.setMaximum(max);
				}
			});
		}

		public void advance(final int value)
		{
			try
			{
				SwingUtilities.invokeAndWait(new Runnable()
				{
					public void run()
					{
						progress.setValue(progress
							.getValue() + value);
					}
				});
				Thread.yield();
			}
			catch(Exception e)
			{
			}
		}

		public void done()
		{
			SwingUtilities.invokeLater(new Runnable()
			{
				public void run()
				{
					currentPage++;
					pageChanged();
				}
			});
		}

		public void error(final String message)
		{
			SwingUtilities.invokeLater(new Runnable()
			{
				public void run()
				{
					dispose();
					JOptionPane.showMessageDialog(null,
						message,
						"Installation aborted",
						JOptionPane.ERROR_MESSAGE);
					System.exit(1);
				}
			});
		}

		public void message(final String message)
		{
			SwingUtilities.invokeLater(new Runnable()
			{
				public void run()
				{
					progress.setString(message);
				}
			});
		}

		public void setThread(InstallThread thread)
		{
			this.thread = thread;
		}
	}
}
