public class OptionsDialogActions extends Observable {


	// Equivalent of runnable that can throw exceptions.
	private interface Command {
		void execute() throws Exception;
	}

	// Undoable command pattern.
	private interface ModificationAction {
		void apply() throws Exception;
		void cancel() throws Exception;
	}

	private static final Queue<ModificationAction> optionModifications =
		new LinkedList<>();

	private static final HexPipeCompleter hpc =
		new HexPipeCompleter();
	private static boolean hexOptionsModified = false;

	private OptionsDialog optionsDialog;
	private boolean modificationsOngoing = false;

	public OptionsDialogActions(OptionsDialog optionsDialog) {
		this.optionsDialog = optionsDialog;
	}


	/**
	 * Sets the close action for the dialog.
	 */
	public void setCloseAction() {
		optionsDialog.setDefaultCloseOperation(
			WindowConstants.DO_NOTHING_ON_CLOSE
		);
		optionsDialog.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				if (optionModifications.size() == 0 ||
					JOptionPane.showConfirmDialog(
						optionsDialog,
						"Do you wish to discard the changes?",
						"Closing options dialog",
						JOptionPane.YES_NO_OPTION
					) == JOptionPane.YES_OPTION) {

					closeWindow();
				}
			}
		});
	}

	/**
	 * Sets the action for a checkbox representing the state of a flag.
	 *
	 * @param flagCheckBox
	 * 			The checkbox.
	 * @param flagField
	 * 			The field containing the flags.
	 * @param valueField
	 * 			The field representing the flag.
	 * @param opts
	 * 			The option object containing the field containing the flags.
	 */
	public void setFlagCheckboxAction(final JCheckBox flagCheckBox,
		final Field flagField, final Field valueField, final Options opts) {

		final Method flagSetter =
			ReflectionUtils.getFlagSetter(opts.getClass(), flagField);
		final Method flagGetter =
			ReflectionUtils.getGetter(opts.getClass(), flagField);

		OptionChangeListener flagCheckboxListener = new OptionChangeListener(
			flagCheckBox, opts, new Command() {
				@Override
				public void execute() throws Exception {
					flagSetter.invoke(
						opts,
						valueField.getLong(null),
						flagCheckBox.isSelected()
					);
				}
			}, new Command() {
				@Override
				public void execute() throws Exception {
					flagCheckBox.setSelected(
						((long) flagGetter.invoke(opts)
							& valueField.getLong(null)) > 0
					);
				}
			}
		);

		flagCheckBox.addActionListener(flagCheckboxListener);
	}

	/**
	 * Sets the action for a component representing an option.
	 *
	 * @param elt
	 * 			The component.
	 * @param optionField
	 * 			The field representing an option.
	 * @param opts
	 * 			The option object containing the field representing an option.
	 */
	@SuppressWarnings("rawtypes")
	public void setOptionComponentAction(JComponent elt, Field optionField,
		Options opts) {

		OptionChangeListener genericListener =
			new OptionChangeListener(elt, opts, optionField);
		if (elt instanceof JCheckBox) {
			((JCheckBox) elt).addActionListener(genericListener);
		} else if (elt instanceof JComboBox) {
			((JComboBox) elt).addActionListener(genericListener);
		} else if (elt instanceof JSpinner) {
			((JSpinner) elt).addChangeListener(genericListener);
		} else {
			throw new IllegalArgumentException(
				"Invalid option modifier component: " + elt
			);
		}
	}

	/**
	 * Sets the action for the "apply" button.
	 *
	 * @param applyButton
	 * 			The button.
	 * @param opts
	 * 			The option object affected by the modifications.
	 */
	public void setApplyButtonAction(JButton applyButton,
		final MainOptions opts) {

		applyButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent ev) {
				applyAllOptionModifications(opts);
			}
		});
	}

	/**
	 * Sets the action for the "ok" button.
	 *
	 * @param okButton
	 * 			The button.
	 * @param opts
	 * 			The option object affected by the modifications.
	 */
	public void setOKButtonAction(JButton okButton, final MainOptions opts) {

		okButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent ev) {
				applyAllOptionModifications(opts);
				optionsDialog.setVisible(false);
			}
		});
	}

	/**
	 * Sets the action for the "cancel" button.
	 *
	 * @param cancelButton
	 * 			The button.
	 * @param opts
	 * 			The option object affected by the modifications.
	 */
	public void setCancelButtonAction(JButton cancelButton) {

		cancelButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent ev) {
				closeWindow();
			}
		});
	}

	/**
	 * Sets the action for the "add replacement" button.
	 *
	 * @param addRowButton
	 * 			The button.
	 * @param tableModel
	 * 			The model of the table containing the replacements.
	 */
	@SuppressWarnings("rawtypes")
	public void setAddRowButtonAction(JButton addRowButton,
		TableViewModelOwner table) {

		setAddRowButtonAction(addRowButton, table, null);
	}

	/**
	 * Sets the action for the "add replacement" button.
	 *
	 * @param addRowButton
	 * 			The button.
	 * @param tableModel
	 * 			The model of the table containing the replacements.
	 * @param additionalAction
	 * 			Something to do in addition to adding a row.
	 */
	@SuppressWarnings("rawtypes")
	public void setAddRowButtonAction(JButton addRowButton,
		final TableViewModelOwner table, final Runnable additionalAction) {

		addRowButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				((DefaultTableModel) table.getModel()).addRow(
					new Object[]{table.getViewModel().getNewRowDefaultModel()}
				);
				if (additionalAction != null) {
					additionalAction.run();
				}
			}
		});
	}

	/**
	 * Sets the action for the "delete selection" button.
	 *
	 * @param deleteSelectedButton
	 * 			The button.
	 * @param tableModel
	 * 			The model of the table containing the replacements.
	 */
	public void setDeleteSelectedButtonAction(JButton deleteSelectedButton,
		final TableViewModelOwner<?> table) {

		setDeleteSelectedButtonAction(deleteSelectedButton, table, 0, null);
	}

	/**
	 * Sets the action for the "delete selection" button.
	 *
	 * @param deleteSelectedButton
	 * 			The button.
	 * @param tableModel
	 * 			The model of the table containing the replacements.
	 * @param minRowCount
	 * 			Minimal number of rows
	 * @param additionalAction
	 * 			Something to do in addition to deleting rows.
	 */
	public void setDeleteSelectedButtonAction(JButton deleteSelectedButton,
		// FIXME: minRowCount is hackish
		final TableViewModelOwner<?> table, final int minRowCount,
		final Runnable additionalAction) {

		deleteSelectedButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				int[] selectedInterval = table.getSelectedRows();
				if (selectedInterval.length == 0) return;

				int firstRowIndex = selectedInterval[0];
				int intervalSize = selectedInterval.length;
				DefaultTableModel model = (DefaultTableModel) table.getModel();
				for (int i = 0; i < intervalSize; ++i) {
					if (model.getRowCount() > minRowCount) {
						model.removeRow(firstRowIndex);
					}
				}
				if (additionalAction != null) {
					additionalAction.run();
				}
			}
		});
	}

	/**
	 * Sets the actions associated to the model of the replacement table.
	 *
	 * @param table
	 * 			The table containing the replacements.
	 * @param isHexOptionsRelated
	 * 			True if the content of the table is part of HexOptions.
	 */
	@SuppressWarnings({ "serial", "rawtypes" })
	public void setTableModelActions(final TableViewModelOwner table,
		boolean isHexOptionsRelated) {

		@SuppressWarnings("unchecked")
		final TableViewModel<Object> viewModel = table.getViewModel();
		final TableData<Object> vmData = viewModel.getData();

		table.setModel(new DefaultTableModel(viewModel.getColumnNames(), 0) {

			private List<Object[]> data = new ArrayList<>();
			private int dataColumnCount =
				viewModel.getArrayFromRowModel(null).length;

			public DefaultTableModel init() {
				for (Object rowModel : vmData.getAll()) {
					Object[] newRow = viewModel.getArrayFromRowModel(rowModel);
					data.add(newRow);
					visualAddRow(newRow);
				}
				return this;
			}

			@Override
			public void insertRow(final int index, final Object[] rowData) {
				final Object rowModel = rowData[0];
				Object[] newRow = viewModel.getArrayFromRowModel(rowModel);
				data.add(index, newRow);
				visualInsertRow(index, newRow);

				optionModifications.add(new ModificationAction() {
					@Override
					public void cancel() throws Exception {
						data.remove(index);
						visualRemoveRow(index);
					}

					@Override
					public void apply() throws Exception {
						vmData.add(rowModel);
					}
				});
			}

			@Override
			public void addRow(Object[] rowData) {
				insertRow(getRowCount(), rowData);
			}

			@Override
			public void removeRow(final int row) {
				final Object[] deletedModel = data.remove(row);
				super.removeRow(row);

				optionModifications.add(new ModificationAction() {
					@Override
					public void cancel() throws Exception {
						data.add(row, deletedModel);
						visualInsertRow(row, deletedModel);
					}

					@Override
					public void apply() throws Exception {
						vmData.remove(row);
					}
				});
			}

			@Override
			public void setValueAt(final Object val, final int row,
				final int column) {

				if (column >= dataColumnCount) return;

				final Object previousVal = data.get(row)[column];
				data.get(row)[column] = val;
				visualSetValueAt(val, row, column);

				optionModifications.add(new ModificationAction() {
					@Override
					public void cancel() throws Exception {
						data.get(row)[column] = previousVal;
						visualSetValueAt(previousVal, row, column);
					}

					@Override
					public void apply() throws Exception {
						viewModel.updateDataFromCoordinates(val, row, column);
					}
				});
			}

			@Override
			public Object getValueAt(int row, int col) {
				return col < dataColumnCount ? data.get(row)[col] : null;
			}

			@Override
			public Class<?> getColumnClass(int col) {
				return viewModel.getColumnClass(col);
			}

			@Override
			public boolean isCellEditable(int row, int col) {
				return col < dataColumnCount &&
					viewModel.isUserEditable(row, col);
			}

			public void visualAddRow(Object[] content) {
				super.addRow(content);
			}

			public void visualInsertRow(int row, Object[] content) {
				super.insertRow(row, content);
			}

			public void visualRemoveRow(int row) {
				super.removeRow(row);
			}

			public void visualSetValueAt(Object aValue, int row, int column) {
				super.setValueAt(aValue, row, column);
			}

		}.init());
	}

	/**
	 * Sets the action for the upwards reordering button of the replacement
	 * table.
	 *
	 * @param table
	 * 			The table containing the replacements.
	 * @param columnIndex
	 * 			The index of the column containing the button.
	 */
	public void addTableUpCellAction(final TableViewModelOwner<?> table,
		final int columnIndex) {

		addTableReorderCellAction(table, columnIndex, true);
	}

	/**
	 * Sets the action for the downwards reordering button of the replacement
	 * table.
	 *
	 * @param table
	 * 			The table containing the replacements.
	 * @param columnIndex
	 * 			The index of the column containing the button.
	 */
	public void addTableDownCellAction(final TableViewModelOwner<?> table,
		final int columnIndex) {

		addTableReorderCellAction(table, columnIndex, false);
	}

	private void addTableReorderCellAction(
		final TableViewModelOwner<?> table, final int columnIndex,
		final boolean up) {

		table.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent evt) {
				int row = table.rowAtPoint(evt.getPoint());
				if ((up && row == 0)
					|| (!up && row == table.getRowCount() - 1)) {

					return;
				}
				int neighbor = row + (up ? -1 : 1);

				int col = table.columnAtPoint(evt.getPoint());
				if (col == columnIndex) {
					DefaultTableModel model =
						(DefaultTableModel) table.getModel();
					for (int i = 0; i < model.getColumnCount(); ++i) {
						Object value = model.getValueAt(neighbor, i);
						model.setValueAt(model.getValueAt(row, i), neighbor, i);
						model.setValueAt(value, row, i);
					}
				}
			}
		});
	}

	/**
	 * Avoids having more than one active radio button at a time in a table
	 * column
	 * @param table
	 * @param row
	 * 		Row to set active
	 * @param col
	 * 		Column containing the radio button
	 */
	public void applyRadioAction(
		TableViewModelOwner<?> table, int row, int col) {

		DefaultTableModel model = (DefaultTableModel) table.getModel();
		for (int i = 0; i < model.getRowCount(); ++i) {
			model.setValueAt(i == row, i, col);
		}
	}

	/**
	 * Activates radio button handling to a table column
	 * @param table
	 * @param columnIndex
	 */
	public void addTableRadioAction(
		final TableViewModelOwner<?> table, final int columnIndex) {

		table.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent evt) {
				int col = table.columnAtPoint(evt.getPoint());
				int row = table.rowAtPoint(evt.getPoint());
				if (col == columnIndex) {
					applyRadioAction(table, row, columnIndex);
				}
			}
		});
	}

	/**
	 * Checks if a the values of a table are currently being saved
	 * @return
	 */
	public boolean isModificationsOngoing() {
		return modificationsOngoing;
	}

	// FIXME: bad design
	/**
	 * Forces the configuration file to be generated from the hex options
	 * the next time modifications are applied
	 */
	public static void setHexOptionsModified () {
		hexOptionsModified = true;
	}

	private void setModificationsOngoing(boolean modificationsOngoing) {
		this.modificationsOngoing = modificationsOngoing;
		setChanged();
		notifyObservers();
	}

	private void applyAllOptionModifications(MainOptions opts) {
		setModificationsOngoing(true);
		try {
			for (ModificationAction modif : optionModifications) {
				modif.apply();
			}
			if (hexOptionsModified) {
				hpc.updateConfig(opts.getHexOptions());
			}
			optionModifications.clear();
			hexOptionsModified = false;
		} catch (Exception e) {
			new GUIErrorHandler(e);
		}
		setModificationsOngoing(false);
	}

	private void cancelAllModifications() {
		setModificationsOngoing(true);
		try {
			ListIterator<ModificationAction> iom =
				new ArrayList<ModificationAction>(optionModifications)
					.listIterator(optionModifications.size());
			while (iom.hasPrevious()) {
				iom.previous().cancel();
			}
			optionModifications.clear();
		} catch (Exception e) {
			new GUIErrorHandler(e);
		}
		setModificationsOngoing(false);
	}


	private void closeWindow() {
		cancelAllModifications();
		optionsDialog.setVisible(false);
	}

	// Generic listener for components associated an option.
	private class OptionChangeListener implements ActionListener,
		ChangeListener {

		private boolean acceptEvent = true;

		private JComponent elt;
		private Options opts;

		private ModificationAction optionModification;

		private OptionChangeListener(final JComponent elt,
			final Options opts, final Field optionField,
			final Command applyCommand,
			final Command cancelCommand) {

			this.elt = elt;
			this.opts = opts;

			this.optionModification = new ModificationAction() {
				@Override
				public void cancel() throws Exception {

					(cancelCommand != null
						? cancelCommand
						: getDefaultCancelCommand(
							ReflectionUtils.getGUIComponentValueSetter(elt),
							ReflectionUtils.getGetter(
								opts.getClass(),
								optionField
							)
						)
					).execute();
				}

				@Override
				public void apply() throws Exception {
					(applyCommand != null
						? applyCommand
						: getDefaultApplyCommand(
							ReflectionUtils.getGUIComponentValueGetter(elt),
							ReflectionUtils.getSetter(
								opts.getClass(),
								optionField
							)
						)
					).execute();
				}
			};
		}

		public OptionChangeListener(final JComponent elt,
			final Options opts, final Field optionField) {

			this(elt, opts, optionField, null, null);
		}

		public OptionChangeListener(final JComponent elt,
			final Options opt, final Command applyCommand,
			final Command cancelCommand) {

			this(elt, opt, null, applyCommand, cancelCommand);
		}

		@Override
		public void stateChanged(ChangeEvent e) {
			actionPerformed(null);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			if (!acceptEvent) return;
			optionModifications.add(this.optionModification);
		}

		private Command getDefaultApplyCommand(
			final Method valGetter, final Method optSetter) {

			return new Command() {
				@Override
				public void execute() throws IllegalAccessException,
					IllegalArgumentException, InvocationTargetException {

					optSetter.invoke(opts, valGetter.invoke(elt));
					if (opts instanceof HexOptions) {
						hexOptionsModified = true;
					}
				}
			};
		}

		private Command getDefaultCancelCommand(
			final Method valSetter, final Method optGetter) {

			return new Command() {
				@Override
				public void execute() throws IllegalAccessException,
					IllegalArgumentException, InvocationTargetException {

					acceptEvent = false;
					valSetter.invoke(elt, optGetter.invoke(opts));
					acceptEvent = true;
				}
			};
		}

	}

} public class GUIErrorHandler {

	public GUIErrorHandler(Exception e) {
		// TODO
		e.printStackTrace();
	}

} public class Images {

	public static final ImageIcon DEFAULT_ICON =
		new ImageIcon(Images.class.getResource("/img/icon.png"));
	public static final ImageIcon DEFAULT_LOGO =
		new ImageIcon(Images.class.getResource("/img/logo.png"));

	public static final ImageIcon TRIANGLE =
		new ImageIcon(Images.class.getResource("/img/triangle.png"));
	public static final ImageIcon INVERTED_TRIANGLE =
		new ImageIcon(Images.class.getResource("/img/inverted_triangle.png"));

	public static ImageIcon resize(ImageIcon icon, int width, int height) {
		return
			new ImageIcon(icon.getImage().getScaledInstance(width, height, 0));
	}

} @SuppressWarnings("serial")
public class HexSelectionsTable extends TableViewModelOwner<HexSelection>
	implements Observer {

	private static final String[] COLUMN_NAMES =
		{"ID", "Start", "End", "Active", "", ""};

	private TableViewModel<HexSelection> viewModel;
	private HexSelections selections;
	private boolean updateOk = true;

	public HexSelectionsTable(HexSelections selections,
		 OptionsDialogActions acts) {

		acts.addObserver(this);
		selections.addObserver(this);
		this.selections = selections;

		final JRadioButton radio = new JRadioButton("", false);
		radio.setHorizontalAlignment(SwingConstants.CENTER);
		DefaultTableCellRenderer booleanRenderer =
			new ToggleButtonRenderer(radio);

		ReorderColumnRenderer reorderRenderer = new ReorderColumnRenderer(
			COLUMN_NAMES.length - 2,
			COLUMN_NAMES.length - 1
		);

		viewModel = makeHexSelectionsViewModel(selections);
		acts.setTableModelActions(this, true);

		DefaultTableCellRenderer hexStringRenderer = getHexStringRenderer();
		getColumnModel().getColumn(1).setCellRenderer(hexStringRenderer);
		getColumnModel().getColumn(2).setCellRenderer(hexStringRenderer);

		getColumnModel().getColumn(3).setCellRenderer(booleanRenderer);
		acts.addTableRadioAction(this, 3);

		getColumnModel().getColumn(4).setCellRenderer(reorderRenderer);
		acts.addTableUpCellAction(this, 4);
		getColumnModel().getColumn(5).setCellRenderer(reorderRenderer);
		acts.addTableDownCellAction(this, 5);

		getColumnModel().getColumn(1).setPreferredWidth(180);
		getColumnModel().getColumn(2).setPreferredWidth(180);
		getColumnModel().getColumn(3).setPreferredWidth(50);
		getColumnModel().getColumn(4).setMaxWidth(20);
		getColumnModel().getColumn(5).setMaxWidth(20);

		TableColumn idCol = getColumnModel().getColumn(0);
		idCol.setMaxWidth(0);
		idCol.setMinWidth(0);
		idCol.setPreferredWidth(0);
		idCol.setResizable(false);

		putClientProperty("terminateEditOnFocusLost", true);
	}

	@Override
	public TableViewModel<HexSelection> getViewModel() {
		return viewModel;
	}

	private DefaultTableCellRenderer getHexStringRenderer() {
		final JLabel hexStringLabel = new JLabel();
		Font font = hexStringLabel.getFont();
		font = font.deriveFont(Collections.singletonMap(
			TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD)
		);
		hexStringLabel.setFont(font);

		return new DefaultTableCellRenderer() {
			@Override
			public Component getTableCellRendererComponent(JTable table,
				Object value, boolean isSelected, boolean hasFocus, int row,
				int column) {

				UIDefaults defaults = javax.swing.UIManager.getDefaults();

				if (isSelected) {
					hexStringLabel.setOpaque(true);
					hexStringLabel.setBackground(
						defaults.getColor("List.selectionBackground")
					);
					hexStringLabel.setForeground(Color.WHITE);
				} else {
					hexStringLabel.setOpaque(false);
					hexStringLabel.setForeground(Color.BLACK);
				}

				hexStringLabel.setText(
					"0x" + String.format("%02X", (Number) value)
				);
				return hexStringLabel;
			}
		};
	}

	private TableViewModel<HexSelection> makeHexSelectionsViewModel(
		final HexSelections selections) {

		return new TableViewModel<HexSelection>() {

			@Override
			public Object[] getArrayFromRowModel(HexSelection model) {
				if (model == null) {
					return new Object[4];
				}

				return new Object[]{
					model.getId(),
					model.getStart(),
					model.getEnd(),
					selections.isActive(model)
				};
			}

			@Override
			public void updateDataFromCoordinates(Object val, int row,
				int column) {

				switch (column) {
				case 0:
					OptionsDialogActions.setHexOptionsModified();
					selections.get(row).setId((Integer) val);
					break;
				case 1:
					selections.get(row).setStart((Long) val);
					break;
				case 2:
					selections.get(row).setEnd((Long) val);
					break;
				case 3:
					if (Boolean.TRUE.equals(val)) {
						OptionsDialogActions.setHexOptionsModified();
						selections.setActiveSelectionIndex(row);
					}
					break;
				}
			}

			@Override
			public Class<?> getColumnClass(int column) {
				if (column == 0) return Integer.class;
				if (column < 3) return Long.class;
				return Boolean.class;
			}

			@Override
			public String[] getColumnNames() {
				return COLUMN_NAMES;
			}

			@Override
			public TableData<HexSelection> getData() {
				return selections;
			}

			@Override
			public HexSelection getNewRowDefaultModel() {
				return new HexSelection(0, 0);
			}

			@Override
			public boolean isUserEditable(int row, int col) {
				return col == 3;
			}
		};
	}

	@Override
	public synchronized void update(Observable o, Object arg) {
		if (o instanceof OptionsDialogActions) {
			updateOk = !((OptionsDialogActions) o).isModificationsOngoing();
		}

		if (!updateOk) return;

		DefaultTableModel m = (DefaultTableModel) this.getModel();
		ArrayList<String> columnList =
			new ArrayList<>(Arrays.asList(COLUMN_NAMES));
		int idCol = columnList.indexOf("ID");
		int activeCol = columnList.indexOf("Active");

		for (int i = 0; i < m.getRowCount(); ++i) {
			int j = 0;
			HexSelection s = selections.getById(
				(Integer) m.getValueAt(i, idCol)
			);
			if (s != null) {
				for (Object v : viewModel.getArrayFromRowModel(s)) {
					// We want to show the selection updates in real time but
					// not change the active selection while the user is
					// using the UI.
					if ((arg == null ||
						!(arg.equals(HexSelections.EXTERNAL_UPDATE) &&
						j == activeCol)) && !v.equals(m.getValueAt(i, j))) {

						m.setValueAt(v, i, j);
					}
					++j;
				}
			}
		}
	}

} @SuppressWarnings("serial")
public class ReplacementsTable extends TableViewModelOwner<Replacement> {

	private static final String[] COLUMN_NAMES =
		{"Sequence", "Replacement", "Escape", "Regex", "Type", "", ""};

	private TableViewModel<Replacement> viewModel;

	public ReplacementsTable(Replacements replacements,
		 OptionsDialogActions acts) {

		final JCheckBox checkbox = new JCheckBox("", false);
		checkbox.setHorizontalAlignment(SwingConstants.CENTER);
		DefaultTableCellRenderer booleanRenderer =
			new ToggleButtonRenderer(checkbox);

		ReorderColumnRenderer reorderRenderer = new ReorderColumnRenderer(
			COLUMN_NAMES.length - 2,
			COLUMN_NAMES.length - 1
		);

		viewModel = makeReplacementsViewModel(replacements);
		acts.setTableModelActions(this, false);

		getColumnModel().getColumn(2).setCellRenderer(booleanRenderer);
		getColumnModel().getColumn(3).setCellRenderer(booleanRenderer);
		getColumnModel().getColumn(4).setCellEditor(new DefaultCellEditor(
			new JComboBox<ReplacementType>(ReplacementType.values())
		));
		getColumnModel().getColumn(5).setCellRenderer(reorderRenderer);
		acts.addTableUpCellAction(this, 5);
		getColumnModel().getColumn(6).setCellRenderer(reorderRenderer);
		acts.addTableDownCellAction(this, 6);

		getColumnModel().getColumn(2).setPreferredWidth(60);
		getColumnModel().getColumn(3).setPreferredWidth(60);
		getColumnModel().getColumn(5).setMaxWidth(20);
		getColumnModel().getColumn(6).setMaxWidth(20);

		putClientProperty("terminateEditOnFocusLost", true);
	}

	@Override
	public TableViewModel<Replacement> getViewModel() {
		return viewModel;
	}

	private TableViewModel<Replacement> makeReplacementsViewModel(
		final Replacements replacements) {

		return new TableViewModel<Replacement>() {

			@Override
			public Object[] getArrayFromRowModel(Replacement model) {
				if (model == null) {
					return new Object[5];
				}

				return new Object[]{
					model.getSequence(),
					model.getReplacement(),
					model.isEscapeCharacters(),
					model.isInterpretAsPattern(),
					model.getType()
				};
			}

			@Override
			public void updateDataFromCoordinates(Object val, int row,
				int column) {

				switch (column) {
				case 0:
					replacements.get(row).setSequence((String) val);
					break;
				case 1:
					replacements.get(row).setReplacement((String) val);
					break;
				case 2:
					replacements.get(row).setEscapeCharacters((Boolean) val);
					break;
				case 3:
					replacements.get(row).setInterpretAsPattern((Boolean) val);
					break;
				case 4:
					replacements.setType(
						replacements.get(row),
						(ReplacementType) val
					);
					break;
				}
			}

			@Override
			public Class<?> getColumnClass(int column) {
				if (column < 2) {
					return String.class;
				} else if (column == 4) {
					return ReplacementType.class;
				}
				return Boolean.class;
			}

			@Override
			public String[] getColumnNames() {
				return COLUMN_NAMES;
			}

			@Override
			public TableData<Replacement> getData() {
				return replacements;
			}

			@Override
			public Replacement getNewRowDefaultModel() {
				return new Replacement(ReplacementType.STR2STR);
			}

			@Override
			public boolean isUserEditable(int row, int col) {
				return true;
			}
		};
	}

} @SuppressWarnings("serial")
public class TablePanel extends JPanel {

	public TablePanel(JTable table, Component[] southContent) {
		super(new BorderLayout());
		add(new JScrollPane(table), BorderLayout.CENTER);

		JPanel southPanel = new JPanel(new FlowLayout());
		for (Component c : southContent) {
			southPanel.add(c);
		}
		add(southPanel, BorderLayout.SOUTH);
	}

} public interface TableViewModel<T> {

	/**
	 * Returns the content of a row in array form
	 * NOTE: Always returns an array of the right length, even if T is null
	 * @param model The model from which the array is constructed
	 * @return
	 */
	public Object[] getArrayFromRowModel(T model);

	/**
	 * Updates the relevant model given coordinates in the table
	 * @param val
	 * @param row
	 * @param column
	 */
	public void updateDataFromCoordinates(Object val, int row, int column);

	/**
	 * Returns the class corresponding to a column
	 * @param column
	 * @return
	 */
	public Class<?> getColumnClass(int column);

	/**
	 * Provides the header of the table
	 * @return
	 */
	public String[] getColumnNames();

	/**
	 * Getter on the underlying data of the table
	 * @return
	 */
	public TableData<T> getData();

	/**
	 * Provides the default model for new rows
	 * @return
	 */
	public T getNewRowDefaultModel();

	/**
	 * Determines whether or not a cell is editable
	 * @param row
	 * @param col
	 * @return
	 */
	public boolean isUserEditable(int row, int col);

} public class HexSelection extends Observable implements Serializable {

	/**
	 * Backward-compatible with 0.9.0
	 */
	private static final long serialVersionUID = 1000000000009000000L;

	private static int nextId = 0;

	private long start;
	private long end;
	private int id;

	public HexSelection(long start, long end) {
		this.start = start;
		this.end = end;
		this.id = nextId++;
	}

	public HexSelection(long start, long end, int id) {
		this.start = start;
		this.end = end;
		this.id = id;
	}

	/**
	 * Getter on the start index of the selection
	 * @return
	 */
	public long getStart() {
		return start;
	}

	/**
	 * Setter on the start index of the selection
	 * @return
	 */
	public void setStart(long start) {
		this.start = start;
		forceNotifyObservers();
	}

	/**
	 * Getter on the end index of the selection
	 * @return
	 */
	public long getEnd() {
		return end;
	}

	/**
	 * Setter on the end index of the selection
	 * @return
	 */
	public void setEnd(long end) {
		this.end = end;
		forceNotifyObservers();
	}

	/**
	 * Getter on the start id of the selection
	 * @return
	 */
	public int getId() {
		return id;
	}

	/**
	 * Setter on the id of the selection. Use with caution.
	 * @return
	 */
	public void setId(int id) {
		this.id = id;
	}

	private void forceNotifyObservers() {
		setChanged();
		notifyObservers();
	}

	private void readObject(ObjectInputStream in) throws IOException,
		ClassNotFoundException {

		in.defaultReadObject();
		if (id >= nextId) nextId = id + 1;
	}

} public class HexSelections extends Observable
	implements TableData<HexSelection>, Serializable, Observer {

	public static final byte EXTERNAL_UPDATE = 0b1;

	/**
	 * Backward-compatible with 0.9.0
	 */
	private static final long serialVersionUID = 1000000000009000000L;

	private List<HexSelection> orderedSelections = new LinkedList<>();
	private int activeSelectionIndex;

	public HexSelections() {
		add(new HexSelection(0, 0));
	}

	public HexSelections(boolean noZeroLengthCheck) {
		if (!noZeroLengthCheck) {
			add(new HexSelection(0, 0));
		}
	}

	@Override
	public HexSelection get(int index) {
		return orderedSelections.get(index);
	}

	@Override
	public List<HexSelection> getAll() {
		return new LinkedList<HexSelection>(orderedSelections);
	}

	@Override
	public void add(HexSelection s) {
		orderedSelections.add(s);
		activeSelectionIndex = orderedSelections.size() - 1;
		s.addObserver(this);
		forceNotifyObservers(null);
	}

	@Override
	public void remove(int index) {
		orderedSelections.remove(index);
		fixInconsistentState();
		forceNotifyObservers(null);
	}

	/**
	 * Setter on the index of the active selection
	 * @param index
	 */
	public void setActiveSelectionIndex(int index) {
		if (index < 0 || index >= orderedSelections.size()) {
			throw new IndexOutOfBoundsException();
		}

		activeSelectionIndex = index;
		forceNotifyObservers(null);
	}

	/**
	 * Getter on the index of the active selection
	 * @return
	 */
	public int getActiveSelectionIndex() {
		return activeSelectionIndex;
	}

	/**
	 * Determines whether a selection is active or not
	 * @param selection
	 * @return
	 */
	public boolean isActive(HexSelection selection) {
		return orderedSelections.indexOf(selection) == activeSelectionIndex;
	}

	/**
	 * Returns a selection by its id
	 * @param id
	 * @return
	 */
	public HexSelection getById(int id) {
		for (HexSelection s : orderedSelections) {
			if (s.getId() == id) return s;
		}
		return null;
	}

	/**
	 * Getter on the active selection
	 * @return
	 */
	public HexSelection getActive() {
		return orderedSelections.get(activeSelectionIndex);
	}

	@Override
	public void update(Observable o, Object arg) {
		forceNotifyObservers(EXTERNAL_UPDATE);
	}

	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		int count = 0;
		res.append("{");
		for (HexSelection s : orderedSelections) {
			res.append("{" + s.getId() + "," + s.getStart() + "," + s.getEnd() +
				"," + isActive(s) + "}");
			if (count < orderedSelections.size() - 1) {
				res.append(",");
			}
			++count;
		}
		res.append("}");
		return res.toString();
	}

	private void fixInconsistentState() {
		if (orderedSelections.isEmpty()) {
			add(new HexSelection(0, 0));
			activeSelectionIndex = 0;
		}
		if (activeSelectionIndex >= orderedSelections.size()) {
			activeSelectionIndex = orderedSelections.size() - 1;
		}
	}

	private void forceNotifyObservers(Object arg) {
		setChanged();
		notifyObservers(arg);
	}

	private void readObject(ObjectInputStream in) throws IOException,
		ClassNotFoundException {

		in.defaultReadObject();
		for (HexSelection s : orderedSelections) {
			s.addObserver(this);
		}
	}

} public class Replacements
	implements TableData<Replacement>, Serializable {

	/**
	 * Backward-compatible with 0.7.0
	 */
	private static final long serialVersionUID = 00000000007000000L;

	private List<Replacement> orderedReplacements;
	private Map<ReplacementType, List<Replacement>> replacementsByType;

	public Replacements() {
		orderedReplacements = new ArrayList<>();
		replacementsByType = new HashMap<>();
		for (ReplacementType type : ReplacementType.values()) {
			replacementsByType.put(type, new LinkedList<Replacement>());
		}
	}

	/**
	 * Getter on a replacement by its index.
	 *
	 * @param index
	 * 			The index of the replacement.
	 * @return The replacement at the specified index.
	 */
	@Override
	public Replacement get(int index) {
		return orderedReplacements.get(index);
	}

	/**
	 * Getter on the replacements.
	 *
	 * @return The replacements.
	 */
	@Override
	public List<Replacement> getAll() {
		return new ArrayList<Replacement>(orderedReplacements);
	}

	/**
	 * Adds a replacement at the end of the list.
	 *
	 * @param r
	 * 			The replacement to add.
	 */
	@Override
	public void add(Replacement r) {
		orderedReplacements.add(r);
		replacementsByType.get(r.getType()).add(r);
	}

	/**
	 * Removes a replacement at the specified index.
	 *
	 * @param index
	 * 			The index of the replacement to remove.
	 */
	@Override
	public void remove(int index) {
		Replacement deleted = orderedReplacements.remove(index);
		replacementsByType.get(deleted.getType()).remove(deleted);
	}

	/**
	 * Empties the list.
	 */
	public void clear() {
		orderedReplacements.clear();
		for (ReplacementType type : replacementsByType.keySet()) {
			replacementsByType.get(type).clear();
		}
	}

	/**
	 * Get all replacements of a specified type.
	 *
	 * @param type
	 * 			The type of the replacements.
	 * @return All replacements of a specified type.
	 */
	public List<Replacement> getByType(ReplacementType type) {
		return replacementsByType.get(type);
	}

	/**
	 * Setter on the type of a replacement.
	 *
	 * @param r
	 * 			The replacement to modify.
	 * @param type
	 * 			The new type of the replacement.
	 */
	public void setType(Replacement r, ReplacementType type) {
		for (ReplacementType previoustType : replacementsByType.keySet()) {
			replacementsByType.get(previoustType).remove(r);
		}
		replacementsByType.get(type).add(r);
		r.setType(type);
	}

	/**
	 * Applies all the replacements of a given type.
	 *
	 * @param s
	 * 			The string to which replacements are applied.
	 * @param type
	 * 			The type of the replacements to use.
	 * @return The string to which replacements were applied.
	 */
	public String apply(String s, ReplacementType type) {
		String res = new String(s);
		for (Replacement r : replacementsByType.get(type)) {
			res = r.apply(res);
		}
		return res;
	}

} public class MainOptions extends Options implements Serializable,
	EncompassingOptions {

	/**
	 * Backward-compatible with 0.7.0
	 */
	private static final long serialVersionUID = 00000000007000000L;

	private HexOptions hexOptions;
	private ConvertOptions convertOptions;
	private GUIOptions GUIOptions;

	private Set<Options> subOptions = new HashSet<>();

	public MainOptions() {
		initSubOptions();
	}

	public MainOptions(String[] args) throws ValueOutOfDomainException {
		this();
		parseArgs(args);
	}

	private void initSubOptions() {
		this.hexOptions = new HexOptions();
		this.convertOptions = new ConvertOptions();
		this.GUIOptions = new GUIOptions();
		updateSubOptionsSet();

	}

	public void updateSubOptionsSet() {
		subOptions.clear();
		subOptions.add(this);
		subOptions.add(this.hexOptions);
		subOptions.add(this.convertOptions);
		subOptions.add(this.GUIOptions);
	}

	/**
	 * Getter on the option objects contained in this class.
	 *
	 * @return The option objects contained in this class.
	 */
	public Set<Options> getSubOptions() {
		return subOptions;
	}

	/**
	 * Getter on the options relative to the behavior of the main lua file.
	 *
	 * @return The options relative to the behavior of the main lua file.
	 */
	public synchronized HexOptions getHexOptions() {
		return hexOptions;
	}

	/**
	 * Getter on the conversion options.
	 *
	 * @return The conversion options.
	 */
	public synchronized ConvertOptions getConvertOptions() {
		return convertOptions;
	}

	/**
	 * Getter of the options for the GUI.
	 *
	 * @return The options for the GUI.
	 */
	public synchronized GUIOptions getGUIOptions() {
		return GUIOptions;
	}

	/**
	 * Parses and applies the command line arguments.
	 *
	 * @param args
	 * 			The command line arguments.
	 * @throws ValueOutOfDomainException
	 */
	public final void parseArgs(String[] args)
		throws ValueOutOfDomainException {

		if (args.length == 1 && args[0].equals("--help")) {
			System.out.println(usage(null, subOptions));
		} else {
			List<String> remainingArgs = Arrays.asList(args);
			for (Options opts : subOptions) {
				OptionsParser parser = new OptionsParser(opts);
				remainingArgs = parser.parse(remainingArgs);
			}
			if (!remainingArgs.isEmpty()) {
				throw new IllegalArgumentException(
					"Illegal command line argument(s) or value(s): "
						+ remainingArgs + "."
				);
			}
		}
	}

} public abstract class Options {

	// used to format usage message
	private static final int INDENT_LENGTH = 4;
	private static final int DESC_LINE_LENGTH = 80 - INDENT_LENGTH;

	public static final String SERIALIAZATION_FILENAME = "config.data";

	protected interface SerializingFallback<T> {
		Charset read(ObjectInputStream in) throws IOException;
		void write(Object t, ObjectOutputStream out) throws IOException;
	}

	public static final SerializingFallback<Charset> CHARSET_IO =
		new SerializingFallback<Charset>() {

		@Override
		public Charset read(ObjectInputStream in) throws IOException {
			return Charsets.getValidCharset(in.readUTF());
		}

		@Override
		public void write(Object t, ObjectOutputStream out)
			throws IOException {

			out.writeUTF(((Charset) t).name());
		}
	};

	/**
	 * Defines how to use command line options.
	 *
	 * @param message
	 * 			The reason why the usage message is printed. May be null.
	 * @return The usage message.
	 */
	public static String usage(String message, Set<Options> subOptions) {
		StringBuilder usage = new StringBuilder();
		if(message != null && !message.isEmpty()) {
			usage.append(message);
			usage.append("\n\nPlease use the following options:\n\n");
		}
		try {
			usage.append(generateUsageMessage(subOptions));
		} catch (IllegalArgumentException | IllegalAccessException
			| NoSuchFieldException | SecurityException
			| NoSuchMethodException e) {

			e.printStackTrace();
		}

		return usage.toString();
	}

	private static String generateUsageMessage(Set<Options> subOptions)
		throws IllegalArgumentException, IllegalAccessException,
		NoSuchFieldException, SecurityException, NoSuchMethodException {

		StringBuilder usage = new StringBuilder();

		Collection<Options> sortedOptions =
			GenericSort.apply(subOptions, null);
		for (Options opt : sortedOptions) {
			Collection<Field> sortedFields = GenericSort.apply(
				ReflectionUtils.getAnnotatedFields(
					opt.getClass(),
					CommandLineArgument.class
				),
				null
			);
			for (Field argField : sortedFields) {
				CommandLineArgument argInfo =
					argField.getAnnotation(CommandLineArgument.class);
				usage.append(getCmdUsage(opt, argField, argInfo));
				usage.append("\n" + getCmdDescription(argInfo));
				if (!argInfo.usageExample().isEmpty()) {
					usage.append("\n" + getCmdExample(argInfo));
				}
				usage.append("\n" + getCmdValueTable(opt, argField));
				usage.append("\n\n");
			}
		}

		usage.append(getHelpCmdUsage());

		return usage.toString();
	}

	private static String getCmdDescription(CommandLineArgument argInfo) {
		return StringUtils.indent(
			StringUtils.breakText(
				argInfo.description(),
				DESC_LINE_LENGTH
			), " ", INDENT_LENGTH
		);
	}

	private static String getCmdExample(CommandLineArgument argInfo) {
		return StringUtils.indent(
			"example: " + argInfo.usageExample(), " ", INDENT_LENGTH
		);
	}

	private static String getCmdUsage(Options opt, Field argField,
		CommandLineArgument argInfo) throws NoSuchMethodException,
		SecurityException, IllegalArgumentException, IllegalAccessException,
		NoSuchFieldException {

		StringBuilder usage = new StringBuilder();

		usage.append("--" + argInfo.command() + "=");
		if (!argInfo.usage().isEmpty()) {
			usage.append(argInfo.usage());
			return usage.toString();
		}

		if (argInfo.flags()) {
			usage.append(" combination of ");
		}

		Domain<?> argDomain = null;
		try {
			argDomain = opt.generateArgumentValueDomain(argField);
			if (argDomain == null) {
				argDomain = opt.getFieldDomain(argField);
			}
		} catch (NoSuchFieldException e) {}
		Collection<Field> valFields = opt.getValueFields(argField);

		if (argDomain != null) {
			usage.append(argDomain);
		} else {
			usage.append("<" + argField.getType().getSimpleName() + ">");
		}

		Object argDefault = opt.getFieldDefaultValue(argField);
		if (argDefault != null) {
			usage.append(", default=");
			if (argInfo.flags()) {
				String flagsStr = DebuggingFlags.longToCmdFlags(
					(Long) opt.getFieldDefaultValue(argField)
				);
				usage.append(flagsStr.isEmpty() ? "none" : flagsStr);
			} else if (valFields == null) {
				usage.append(argDefault);
			} else {
				usage.append(
					getDefaultValueString(argDefault, valFields)
				);
			}
		}

		return usage.toString();
	}

	private static String getCmdValueTable(Options opt, Field argField)
		throws NoSuchMethodException, SecurityException,
		IllegalArgumentException, IllegalAccessException {

		Collection<CommandLineValue> valDescs;
		try {
			valDescs = opt.getValuesDescriptions(
				opt.getFieldValueClass(argField)
			);
			return StringUtils.indent(
				generateValueTable(valDescs),
				" ",
				INDENT_LENGTH
			);
		} catch (NoSuchFieldException e) {}

		return "";
	}

	private static String getDefaultValueString(Object defaultValue,
		Collection<Field> valFields) throws IllegalArgumentException,
		IllegalAccessException {

		for (Field valField : valFields) {
			if (valField.get(null).toString().equals(defaultValue.toString())) {
				return valField.getAnnotation(CommandLineValue.class).value();
			}
		}
		return defaultValue.toString();
	}

	private static String getHelpCmdUsage() {
		return "--help (cannot be used with other options)\n"
			+ StringUtils.indent("Displays this message.", " ", INDENT_LENGTH);
	}

	/**
	 * Getter on the possible values of a configurable member.
	 *
	 * @param argField
	 * 			The configurable member.
	 * @return The possible values of the configurable member.
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws SecurityException
	 */
	public Collection<Field> getValueFields(Field argField)
		throws IllegalArgumentException, IllegalAccessException,
		SecurityException {

		Class<?> argValsClass = null;
		Collection<Field> valFields;
		try {
			argValsClass = getFieldValueClass(argField);
			valFields = ReflectionUtils.getAnnotatedFields(
				argValsClass,
				CommandLineValue.class
			);
		} catch (NoSuchFieldException e) {
			valFields = null;
		}
		return valFields;
	}

	private Collection<CommandLineValue> getValuesDescriptions(
		Class<?> valueClass) throws NoSuchMethodException,
		SecurityException {

		return valueClass == null
			? null
			: GenericSort.apply(
				ReflectionUtils.getAnnotations(
					valueClass,
					CommandLineValue.class
				),
				CommandLineValue.class.getMethod("value")
			);
	}

	/**
	 * Maps the command line string values to their actual value.
	 *
	 * @param argField
	 * 			A configurable member with a restricted number of values.
	 * @return A map of the command line string values to their actual value.
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	@SuppressWarnings("unchecked")
	public <T> Map<String, T> getValueDomainToActualValue(Field argField)
		throws NoSuchMethodException, SecurityException,
		IllegalArgumentException, IllegalAccessException {

		Collection<Field> valFields = getValueFields(argField);
		if (valFields == null) {
			return null;
		}

		Map<String, T> res = new HashMap<>();

		for (Field valField : valFields) {
			CommandLineValue valDesc =
				valField.getAnnotation(CommandLineValue.class);
			String key = valDesc.value().isEmpty()
				? valField.get(null).toString()
				: valDesc.value();
			res.put(key, (T) valField.get(null));
		}

		return res;
	}

	private Values<String> generateArgumentValueDomain(Field argField)
		throws NoSuchMethodException, SecurityException,
		IllegalArgumentException, IllegalAccessException {

		Map<String, Object> valuesMap = getValueDomainToActualValue(argField);
		if (valuesMap == null) {
			return null;
		}
		return new Values<String>(
			valuesMap.keySet().toArray(new String[valuesMap.keySet().size()])
		);
	}

	private static String generateValueTable(
		Collection<CommandLineValue> valuesDesc) {

		final String VALUE = "value";
		final String DESCRIPTION = "description";
		final String SHORTCUT = "shortcut";
		final String CONDITION = "condition";

		final String COLUMN_SEPARATOR = "   ";
		final String HEADER_SEPARATOR = "-";

		Map<Integer, String> headers = new TreeMap<>();
		headers.put(0, VALUE);
		Map<String, Map<String, String>> content = new TreeMap<>();
		for (CommandLineValue desc : valuesDesc) {
			Map<String, String> lineContent = new HashMap<>();
			lineContent.put(DESCRIPTION, desc.description());
			headers.put(2, DESCRIPTION);
			if (desc.shortcut().length() > 0) {
				lineContent.put(SHORTCUT, "-" + desc.shortcut());
				headers.put(1, SHORTCUT);
			}
			if (desc.condition().length() > 0) {
				lineContent.put(CONDITION, desc.condition());
				headers.put(3, CONDITION);
			}
			content.put(desc.value(), lineContent);
		}

		StringBuilder table = new StringBuilder();
		int[] widths = new int[headers.size()];
		Arrays.fill(widths, 30);
		widths[0] = 10;
		if (headers.containsValue(SHORTCUT)) {
			widths[1] = 10;
		}

		int eltCounter = 0;
		for (String title : headers.values()) {
			table.append(
				StringUtils.fillWithSpaces(title, widths[eltCounter++])
			);
			table.append(COLUMN_SEPARATOR);
		}
		table.append("\n");
		eltCounter = 0;
		for (String title : headers.values()) {
			table.append(
				StringUtils.fillWithSpaces(
					title.replaceAll(".", HEADER_SEPARATOR),
					widths[eltCounter++]
				)
			);
			table.append(COLUMN_SEPARATOR);
		}
		table.append("\n");
		headers.remove(0);
		for (String value : content.keySet()) {
			String[] paragraphs = new String[headers.values().size() + 1];
			paragraphs[0] = value;
			int columnIndex = 1;
			for (String title : headers.values()) {
				paragraphs[columnIndex++] = content.get(value).get(title);
			}
			table.append(
				StringUtils.putParagraphsSideBySide(
					paragraphs,
					widths,
					COLUMN_SEPARATOR
				)
			);
		}

		return table.toString();
	}

	/**
	 * Getter on the default value of a configurable member.
	 *
	 * @param argField
	 * 			The configurable member.
	 * @return The default value of the configurable member.
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws NoSuchFieldException
	 * @throws SecurityException
	 */
	public Object getFieldDefaultValue(Field argField)
		throws IllegalArgumentException, IllegalAccessException,
		NoSuchFieldException, SecurityException {

		return getFieldAssociatedInformation("DEFAULT_", argField, "");
	}

	/**
	 * Getter on the value domain of a configurable member.
	 *
	 * @param @param argField
	 * 			The configurable member.
	 * @return The value domain of the configurable member.
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws NoSuchFieldException
	 * @throws SecurityException
	 */
	public Domain<?> getFieldDomain(Field argField)
		throws IllegalArgumentException, IllegalAccessException,
		NoSuchFieldException, SecurityException {

		return (Domain<?>)
			getFieldAssociatedInformation("", argField, "_DOMAIN");
	}

	/**
	 * Getter on the value class of a configurable member.
	 *
	 * @param argField
	 * 			The configurable member.
	 * @return The value class of the configurable member.
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws NoSuchFieldException
	 * @throws SecurityException
	 */
	@SuppressWarnings("unchecked")
	public Class<? extends ValueClass> getFieldValueClass(Field argField)
		throws IllegalArgumentException, IllegalAccessException,
		NoSuchFieldException, SecurityException {

		return (Class<? extends ValueClass>)
			getFieldAssociatedInformation("", argField, "_VALUE_CLASS");
	}

	/**
	 * Getter on the value class of a configurable member.
	 *
	 * @param argField
	 * 			The configurable member.
	 * @return
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws NoSuchFieldException
	 * @throws SecurityException
	 */
	@SuppressWarnings("unchecked")
	public Class<? extends ArgumentParser<?>> getFieldParser(Field argField)
		throws IllegalArgumentException, IllegalAccessException,
		NoSuchFieldException, SecurityException {

		return (Class<? extends ArgumentParser<?>>)
			getFieldAssociatedInformation("", argField, "_PARSER");
	}

	private Object getFieldAssociatedInformation(String prefix,
		Field argField, String suffix) throws IllegalArgumentException,
		IllegalAccessException, NoSuchFieldException, SecurityException {

		Field f = getClass().getDeclaredField(
			prefix
				+ StringUtils.camelToScreamingSnake(argField.getName())
				+ suffix
		);
		f.setAccessible(true);
		return f.get(this);
	}

	protected synchronized void serialize(ObjectOutputStream out)
		throws IOException {

		out.defaultWriteObject();
		try {
			Map<Field, SerializingFallback<?>> fallbacks =
				getSerializingFallbacks();
			for (Field f : fallbacks.keySet()) {
				fallbacks.get(f).write(f.get(this), out);
			}
		} catch (IllegalArgumentException | IllegalAccessException
			| NoSuchFieldException | SecurityException e) {

			throw new IOException(e);
		}
	}

	protected synchronized void deserialize(ObjectInputStream in)
		throws IOException, ClassNotFoundException {

		in.defaultReadObject();
		try {
			Map<Field, SerializingFallback<?>> fallbacks =
				getSerializingFallbacks();
			for (Field f : fallbacks.keySet()) {
				f.set(this, fallbacks.get(f).read(in));
			}
		} catch (IllegalArgumentException | IllegalAccessException
			| NoSuchFieldException | SecurityException e) {

			throw new IOException(e);
		}
	}

	protected Map<Field, SerializingFallback<?>> getSerializingFallbacks()
		throws IllegalArgumentException, IllegalAccessException,
		NoSuchFieldException, SecurityException {

		List<Field> nonSerializableFields = ReflectionUtils.getModifiedFields(
			this.getClass(),
			Modifier.TRANSIENT
		);
		Map<Field, SerializingFallback<?>> fallbacks = new HashMap<>();
		for (Field f : nonSerializableFields) {
			f.setAccessible(true);
			String fallbackName = StringUtils.camelToScreamingSnake(
				f.getType().getSimpleName()
			) + "_IO";
			fallbacks.put(
				f,
				(SerializingFallback<?>) Options.class.getField(fallbackName)
					.get(this)
			);
		}
		return fallbacks;
	}

} public class Bounds<T extends Comparable<T>> implements Domain<T> {

	private T min;
	private T max;

	public Bounds(T min, T max) {
		this.min = min;
		this.max = max;
	}

	/**
	 * Getter on the minimum (inclusive) bound of the domain.
	 *
	 * @return The minimum (inclusive) bound of the domain.
	 */
	public T getMin() {
		return min;
	}

	/**
	 * Getter on the maximum (inclusive) bound of the domain.
	 *
	 * @return The maximum (inclusive) bound of the domain.
	 */
	public T getMax() {
		return max;
	}

	@Override
	public boolean inDomain(T value) {
		return value.compareTo(min) >= 0 && value.compareTo(max) <= 0;
	}

	@Override
	public String toString() {
		return "[" + min + ", " + max + "]";
	}

} public interface Domain<T> {

	/**
	 * Determines whether a value is in the domain or not.
	 *
	 * @param value
	 * 			The value to check.
	 * @return True if the value is in the domain.
	 */
	boolean inDomain(T value);

} public class Values<T> implements Domain<T> {

	private T[] values;

	@SuppressWarnings("unchecked")
	public Values(T[] values) {
		Set<T> noDuplicates = new HashSet<>();
		for (T value : values){
			noDuplicates.add(value);
		}
		this.values = (T[]) noDuplicates.toArray();
	}

	/**
	 * Getter on the possible values.
	 *
	 * @return The possible values.
	 */
	public T[] getValues() {
		return values;
	}

	@Override
	public boolean inDomain(T value) {
		for (T acceptedValue : values) {
			if (value.equals(acceptedValue)) {
				return true;
			}
		}
		return false;
	}

	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append("{");
		int counter = 0;
		for (T acceptedValue : values) {
			res.append(acceptedValue);
			if (counter < values.length - 1) {
				res.append(", ");
			}
			++counter;
		}
		res.append("}");
		return res.toString();
	}

} public class GenericSort {

	/**
	 * Alphabetically sorts a generic collection.
	 * Warning: duplicates are deleted.
	 *
	 * @param c
	 * 			The collection to sort
	 * @param getString
	 * 			The method used to sort the collection.
	 * @return The sorted collection.
	 */
	public static <T> Collection<T> apply(Collection<T> c, Method getString) {
		TreeMap<String, T> sorter = new TreeMap<>();
		for (T elt : c) {
			if (getString == null) {
				sorter.put(elt.toString(), elt);
			} else {
				try {
					sorter.put((String) getString.invoke(elt), elt);
				} catch (IllegalAccessException | IllegalArgumentException
					| InvocationTargetException e) {

					sorter.put(elt.toString(), elt);
				}
			}
		}
		return sorter.values();
	}

} public class ReflectionUtils {

	/**
	 * Finds the getters of a class.
	 *
	 * @param c
	 * 			The class.
	 * @return The getters of the class.
	 */
	public static List<Method> getGetters(Class<?> c) {
		Method[] allMethods = c.getDeclaredMethods();
		List<Method> getters = new LinkedList<>();
		for (Method m : allMethods) {
			if (((m.getReturnType().equals(boolean.class)
				&& m.getName().startsWith("is"))
				|| m.getName().startsWith("get"))
				&& m.getParameterTypes().length == 0) {

				getters.add(m);
			}
		}
		return getters;
	}

	/**
	 * Finds the setters of a class.
	 *
	 * @param c
	 * 			The class.
	 * @return The setters of the class.
	 */
	public static List<Method> getSetters(Class<?> c) {
		return getSetters(c, false);
	}

	private static List<Method> getSetters(Class<?> c, boolean flag) {
		Method[] allMethods = c.getDeclaredMethods();
		List<Method> setters = new LinkedList<>();
		for (Method m : allMethods) {
			Class<?>[] types = m.getParameterTypes();
			if (m.getName().startsWith("set")
				&& types.length == (flag ? 2 : 1)) {

				if ((flag && types[1].equals(boolean.class)) || !flag) {
					setters.add(m);
				}
			}
		}
		return setters;
	}

	private static Method getAccessor(Class<?> c, Field field, boolean setter,
		boolean flag) {

		String accessorSuffix;
		int modifiers = field.getModifiers();
		if (Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers)) {
			accessorSuffix = StringUtils.screamingSnakeToWords(field.getName());
		} else {
			accessorSuffix = StringUtils.camelToWords(field.getName());
		}
		accessorSuffix = StringUtils.wordsToScreamingCamel(accessorSuffix);

		List<Method> accessors = setter ? getSetters(c, flag) : getGetters(c);
		for (Method accessor : accessors) {
			if (accessor.getName().endsWith(accessorSuffix)) {
				return accessor;
			}
		}
		return null;
	}

	/**
	 * Finds the setter of a field.
	 *
	 * @param c
	 * 			The class containing the field.
	 * @param field
	 * 			The field.
	 * @return The setter of the field.
	 */
	public static Method getSetter(Class<?> c, Field field) {
		return getAccessor(c, field, true, false);
	}

	/**
	 * Finds the getter of a field.
	 *
	 * @param c
	 * 			The class containing the field.
	 * @param field
	 * 			The field.
	 * @return The getter of the field.
	 */
	public static Method getGetter(Class<?> c, Field field) {
		return getAccessor(c, field, false, false);
	}

	/**
	 * Finds the setter of a field containing flags.
	 *
	 * @param c
	 * 			The class containing the field.
	 * @param field
	 * 			The field.
	 * @return The setter of the field.
	 */
	public static Method getFlagSetter(Class<?> c, Field field) {
		return getAccessor(c, field, true, true);
	}

	/**
	 * Finds the fields annotated by a given annotation.
	 *
	 * @param c
	 * 			The class containing the fields.
	 * @param annotationClass
	 * 			The annotation.
	 * @return The fields annotated by the annotation.
	 */
	public static List<Field> getAnnotatedFields(Class<?> c,
		Class<? extends Annotation> annotationClass) {

		Field[] fields = c.getDeclaredFields();
		List<Field> annotatedFields = new LinkedList<>();
		for (Field field : fields) {
			if (field.getAnnotation(annotationClass) != null) {
				annotatedFields.add(field);
			}
		}
		return annotatedFields;
	}

	/**
	 * Finds the annotation objects of annotated fields.
	 *
	 * @param c
	 * 			The class containing the fields.
	 * @param annotationClass
	 * 			The annotation.
	 * @return The annotations of the annotated fields.
	 */
	public static <T extends Annotation> List<T> getAnnotations(Class<?> c,
		Class<T> annotationClass) {

		Field[] fields = c.getDeclaredFields();
		List<T> annotations = new LinkedList<>();
		for (Field field : fields) {
			T annotation = field.getAnnotation(annotationClass);
			if (annotation != null) {
				annotations.add(annotation);
			}
		}
		return annotations;
	}

	/**
	 * Finds the fields with the given modifiers.
	 *
	 * @param c
	 * 			The class containing the fields.
	 * @param modifiers
	 * 			The modifiers.
	 * @return The fields with the given modifiers.
	 */
	public static List<Field> getModifiedFields(Class<?> c, int modifiers) {
		Field[] fields = c.getDeclaredFields();
		List<Field> modifiedFields = new LinkedList<>();
		for (Field field : fields) {
			if ((field.getModifiers() & modifiers) > 0) {
				modifiedFields.add(field);
			}
		}
		return modifiedFields;
	}

	private static Method getGUIComponentValueAccessor(Component elt,
		boolean setter) {

		String accessorPrefix = setter ? "set" : "get";
		Class<?>[] args = new Class<?>[0];
		try {
			if (elt instanceof JCheckBox) {
				if (setter) {
					args = new Class<?>[]{boolean.class};
				} else {
					accessorPrefix = "is";
				}
				return elt.getClass().getMethod(
					accessorPrefix + "Selected",
					args
				);
			} else if (elt instanceof JComboBox
				|| elt instanceof JSpinner) {

				if (setter) {
					args = new Class<?>[]{Object.class};
				}
				String accessorSuffix = elt instanceof JComboBox
					? "SelectedItem"
					: "Value";
				return elt.getClass().getMethod(
					accessorPrefix + accessorSuffix,
					args
				);
			}
		} catch (NoSuchMethodException | SecurityException e) {
			new GUIErrorHandler(e);
		}
		return null;
	}

	/**
	 * Finds the getter on the value contained in a component.
	 *
	 * @param elt
	 * 			The graphical component containing a value.
	 * @return The getter on the value contained in the component.
	 */
	public static Method getGUIComponentValueGetter(Component elt) {
		return getGUIComponentValueAccessor(elt, false);
	}

	/**
	 * Finds the setter on the value contained in a component.
	 *
	 * @param elt
	 * 			The graphical component containing a value.
	 * @return The setter on the value contained in the component.
	 */
	public static Method getGUIComponentValueSetter(Component elt) {
		return getGUIComponentValueAccessor(elt, true);
	}

} public class StringUtils {

	/**
	 * Indents a string.
	 *
	 * @param s
	 * 			The string to indent.
	 * @param tab
	 * 			The string used as an indentation.
	 * @param nbTabs
	 * 			The number of indentations.
	 * @return An indented string.
	 */
	public static String indent(String s, String tab, int nbTabs) {
		Scanner scanner = new Scanner(s);
		StringBuilder tabsBuilder = new StringBuilder();
		for (int i = 0; i < nbTabs; ++i) {
			tabsBuilder.append(tab);
		}
		String tabs = tabsBuilder.toString();

		StringBuilder indentedString = new StringBuilder();
		while (scanner.hasNextLine()) {
			indentedString.append(tabs);
			indentedString.append(scanner.nextLine());
			if (scanner.hasNextLine()) {
				indentedString.append("\n");
			}
		}

		scanner.close();

		return indentedString.toString();
	}

	/**
	 * Translates "A_STRING" into "a string"
	 * @param name
	 * 			THE_STRING
	 * @return "the string"
	 */
	public static String screamingSnakeToWords(String name) {
		return name.replace("_", " ").toLowerCase(Locale.ENGLISH);
	}

	/**
	 * Translates "aString" into "a string"
	 * @param name
	 * 			theString
	 * @return "the string"
	 */
	public static String camelToWords(String name) {
		return name.replaceAll("([a-z])([A-Z])", "$1 $2")
			.toLowerCase(Locale.ENGLISH);
	}

	/**
	 * Translates "a string" into "AString"
	 * @param name
	 * 			"the string"
	 * @return TheString
	 */
	public static String wordsToScreamingCamel(String words) {
		String[] splitWords = words.split(" ");
		StringBuffer camel = new StringBuffer();

		for (int i = 0; i < splitWords.length; i++) {
			camel.append(StringUtils.capitalize(splitWords[i]));
		}
		return camel.toString();
	}

	/**
	 * Translates "a string" into "A_STRING"
	 * @param name
	 * 			"the string"
	 * @return THE_STRING
	 */
	public static String wordsToScreamingSnake(String words) {
		return words.replace(" ", "_").toUpperCase(Locale.ENGLISH);
	}

	/**
	 * Translates "aString" into "A_STRING"
	 * @param name
	 * 			theString
	 * @return THE_STRING
	 */
	public static String camelToScreamingSnake(String name) {
		return wordsToScreamingSnake(camelToWords(name));
	}

	public static String capitalize(String s) {
		return (s.charAt(0) + "").toUpperCase(Locale.ENGLISH) + s.substring(1);
	}

	/**
	 * Translates HTML into plain text.
	 *
	 * @param HTML
	 * 			The HTML.
	 * @return The plain text version of the HTML.
	 */
	public static String HTMLToPlainText(String HTML) {
		return HTML
			.replaceAll("<br>", "\n")
			.replaceAll("<.*?>", "")
			.replace("&nbsp;&nbsp;", "\t");
	}

	/**
	 * Translates plain text into HTML.
	 *
	 * @param HTML
	 * 			The plain text.
	 * @return The HTML version of the plain text.
	 */
	public static String plainTextToHTML(String txt) {
		return "<html>" + txt
			.replace("\n", "<br>")
			.replace("\t", "&nbsp;&nbsp;") + "</html>";
	}

	/**
	 * Breaks a line of text into several lines without considering separators.
	 *
	 * @param txt
	 * 			The text to break.
	 * @param maxLineLength
	 * 			The maximum number of characters in a single line.
	 * @return The broken text.
	 */
	public static String breakNoSpaceText(String txt, int maxLineLength) {
		StringBuilder brokenString = new StringBuilder();
		int charCount = 0;
		while (charCount < txt.length()) {
			int nextLineBreakInd = txt.substring(charCount).indexOf('\n');
			int nextCharInd, prevCharCount = charCount;
			if (nextLineBreakInd == -1 || nextLineBreakInd > maxLineLength) {
				nextCharInd = Math.min(charCount + maxLineLength, txt.length());
				charCount += maxLineLength;
			} else {
				nextCharInd =
					Math.min(charCount + nextLineBreakInd, txt.length());
				charCount += nextLineBreakInd + 1;
			}
			brokenString.append(txt.substring(prevCharCount, nextCharInd));
			brokenString.append("\n");
		}
		return brokenString.toString();
	}

	/**
	 * Breaks a line of text into several lines using existing separators.
	 *
	 * @param txt
	 * 			The text to break.
	 * @param maxLineLength
	 * 			The maximum number of characters in a single line.
	 * @return The broken text.
	 */
	public static String breakText(String txt, int maxLineLength) {
		StringTokenizer st = new StringTokenizer(txt, " ");
		StringBuilder brokenString = new StringBuilder(txt.length());
		int lineLength = 0;
		while (st.hasMoreTokens()) {
			String word = st.nextToken();

			boolean nextLine = lineLength + word.length() > maxLineLength;
			if (!nextLine && lineLength > 0) {
				brokenString.append(" ");
			}
			if (nextLine) {
				brokenString.append("\n");
				lineLength = 0;
			}
			brokenString.append(word);
			lineLength += word.length() + 1;
			if (word.contains("\n")) {
				brokenString.append(" ");
				lineLength = 0;
			}
		}
		return brokenString.toString();
	}

	/**
	 * Stuffs the end of a string with spaces.
	 *
	 * @param s
	 * 			The string to stuff.
	 * @param targetSize
	 * 			The end size of the string.
	 * @return The stuffed string.
	 */
	public static String fillWithSpaces(String s, int targetSize) {
		StringBuilder sb = new StringBuilder(s);
		for (int i = 0; i < targetSize - s.length(); ++i) {
			sb.append(" ");
		}
		return sb.toString().substring(0, targetSize);
	}

	/**
	 * Puts paragraphs side-by-side.
	 *
	 * @param paragraphs
	 * 			The paragraphs.
	 * @param widths
	 * 			The maximum width of a line in each paragraph.
	 * @param separator
	 * 			The separator to use between the lines of the paragraphs.
	 * @return The paragraphs side-by-side.
	 */
	public static String putParagraphsSideBySide(String[] paragraphs,
		int[] widths, String separator) {

		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < paragraphs.length; ++i) {
			if (paragraphs[i] == null) {
				paragraphs[i] = "";
			} else {
				paragraphs[i] = breakText(paragraphs[i], widths[i]);
			}
		}
		boolean addedLine;
		do {
			StringBuilder newLine = new StringBuilder();
			addedLine = false;
			boolean addedCell = false;
			for (int i = 0; i < paragraphs.length; ++i) {
				if (paragraphs[i].length() > 0) {
					addedCell = true;
				}
				int lineEndIndex = paragraphs[i].indexOf("\n");
				if (lineEndIndex == -1) {
					lineEndIndex = paragraphs[i].length();
				}
				String line = paragraphs[i].substring(0, lineEndIndex);
				if (!line.equals(paragraphs[i])) {
					paragraphs[i] =
						paragraphs[i].substring(lineEndIndex + 1);
				} else {
					paragraphs[i] = "";
				}
				newLine.append(fillWithSpaces(line, widths[i]));
				if (i < paragraphs.length - 1) {
					newLine.append(separator);
				}
			}
			if (addedCell) {
				addedLine = true;
				sb.append(newLine);
				sb.append("\n");
			}
		} while (addedLine);

		return sb.toString();
	}

}
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.table.DefaultTableModel;
import gui.actions.OptionsDialogActions;
import gui.utils.GUIErrorHandler;
import gui.utils.Images;
import gui.views.components.HexSelectionsTable;
import gui.views.components.ReplacementsTable;
import gui.views.components.TablePanel;
import gui.views.models.TableViewModel;
import hexcapture.HexSelection;
import hexcapture.HexSelections;
import hextostring.replacement.Replacements;
import main.MainOptions;
import main.options.Options;
import main.options.annotations.CommandLineArgument;
import main.options.annotations.CommandLineValue;
import main.options.domain.Bounds;
import main.options.domain.Domain;
import main.options.domain.Values;
import main.utils.GenericSort;
import main.utils.ReflectionUtils;
import main.utils.StringUtils;
package gui.views;




/**
 * The dialog containing all the necessary elements to modify the options.
 *
 * @author Maxime PIA
 */
@SuppressWarnings("serial")
public class OptionsDialog extends JDialog {

	private OptionsDialogActions acts = new OptionsDialogActions(this);

	private MainOptions opts;

	public static final double DOUBLE_SPINNER_STEP = 0.005;
	public static final int INTEGER_SPINNER_STEP = 1;

	public OptionsDialog(MainOptions opts) {
		setTitle("Options");
		setModal(true);
		setIconImage(Images.DEFAULT_ICON.getImage());