package com.swingsane.gui.controller;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

import javax.swing.ComboBoxModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import org.apache.log4j.Logger;

import au.com.southsky.jfreesane.RateLimitingScanListeners;
import au.com.southsky.jfreesane.SaneDevice;
import au.com.southsky.jfreesane.SaneException;
import au.com.southsky.jfreesane.ScanListener;
import au.com.southsky.jfreesane.ScanListenerAdapter;

import com.swingsane.business.discovery.DiscoveryEvent;
import com.swingsane.business.discovery.DiscoveryJob;
import com.swingsane.business.discovery.DiscoveryListener;
import com.swingsane.business.notification.INotification;
import com.swingsane.business.options.KnownSaneOptions;
import com.swingsane.business.scanning.IScanService;
import com.swingsane.business.scanning.ScanEvent;
import com.swingsane.business.scanning.ScanEventListener;
import com.swingsane.business.scanning.ScanJob;
import com.swingsane.gui.dialog.CustomSettingsDialog;
import com.swingsane.gui.dialog.GlobalSettingsDialog;
import com.swingsane.gui.dialog.ScannerSettingsDialog;
import com.swingsane.gui.list.ScannerListItem;
import com.swingsane.gui.list.ScannerListItem.ScannerStatus;
import com.swingsane.i18n.Localizer;
import com.swingsane.preferences.IPreferredDefaults;
import com.swingsane.preferences.ISwingSanePreferences;
import com.swingsane.preferences.PreferencesUtils;
import com.swingsane.preferences.model.ApplicationPreferences;
import com.swingsane.preferences.model.Login;
import com.swingsane.preferences.model.Scanner;
import com.swingsane.util.Misc;
import com.thoughtworks.xstream.XStream;

public class ComponentController implements INotification {

  /**
   * Log4J logger.
   */
  private static final Logger LOG = Logger.getLogger(ComponentController.class);

  private IComponents components;
  private IScanEventHandler scanEventHandler;
  private IScanService scanService;
  private IPreferredDefaults preferredDefaults;
  private ISwingSanePreferences preferences;

  private DiscoveryJob discoveryJob;
  private DiscoveryListener discoveryListener;

  private static final int PROGRESS_BAR_UPDATE_INTERVAL = 100;

  private DefaultListModel<ScannerListItem> scannerListModel = new DefaultListModel<ScannerListItem>();

  private XStream xstream;

  public ComponentController() {
  }

  @Override
  public void addAbortListener() {
  }

  private void addMessage(String message) {
    StyledDocument doc = components.getMessagesTextPane().getStyledDocument();
    SimpleAttributeSet keyWord = new SimpleAttributeSet();
    StyleConstants.setForeground(keyWord, Color.GREEN);
    StyleConstants.setBackground(keyWord, Color.BLUE);
    StyleConstants.setBold(keyWord, true);
    try {
      if (message.toLowerCase().startsWith("found ")) {
        doc.insertString(0, message + "\n", keyWord);
      } else {
        doc.insertString(0, message + "\n", null);
      }
    } catch (BadLocationException e) {
      LOG.error(e, e);
    }
  }

  private void addScannerButtonActionPerformed(ActionEvent e) {
    ScannerSettingsDialog scannerSettingsDialog = new ScannerSettingsDialog(
        components.getRootComponent());
    scannerSettingsDialog.setTitle(Localizer.localize("AddScannerDialogTitle"));
    scannerSettingsDialog.setModal(true);
    scannerSettingsDialog.setVisible(true);
    if (scannerSettingsDialog.getDialogResult() == JOptionPane.OK_OPTION) {
      try {
        detectScanners(scannerSettingsDialog.getRemoteAddress(),
            scannerSettingsDialog.getPortNumber(), scannerSettingsDialog.getDescription());
      } catch (UnknownHostException ex) {
        LOG.error(ex, ex);
      } catch (IOException ex) {
        LOG.error(ex, ex);
      } catch (SaneException ex) {
        LOG.error(ex, ex);
      }
    }
  }

  private void adjustSettingsComponents(Scanner scanner) {

    boolean isUsingCustomOptions = scanner.isUsingCustomOptions();

    components.getSaveSettingsButton().setEnabled(true);
    components.getUseCustomSettingsCheckBox().setEnabled(true);
    components.getUseCustomSettingsCheckBox().setSelected(isUsingCustomOptions);
    components.getCustomSettingsButton().setEnabled(isUsingCustomOptions);

    if (!isUsingCustomOptions) {
      updateResolutionModel(KnownSaneOptions.getResolutionModel(scanner),
          KnownSaneOptions.getResolution(scanner));
      updatePageSizeModel(KnownSaneOptions.getPageSizeModel(scanner),
          KnownSaneOptions.getPageSize(scanner));
      updateColorModeModel(KnownSaneOptions.getColorModeComponent(scanner),
          KnownSaneOptions.getColorMode(scanner));
      updateDuplexCheckBox(KnownSaneOptions.isDuplexScanningAvailable(scanner),
          KnownSaneOptions.isDuplexScanningEnabled(scanner));
      updateAutoCropCheckBox(KnownSaneOptions.isAutoCropAvailable(scanner),
          KnownSaneOptions.isAutoCropSelected(scanner));
      updateBlackThresholdModel(KnownSaneOptions.getBlackThresholdModel(scanner));
      updateUsingBlackThresholdCheckBox(KnownSaneOptions.isUsingDefaultBlackThreshold(scanner));
      updateSourceModel(KnownSaneOptions.getSourceModel(scanner),
          KnownSaneOptions.getSource(scanner));
    }

    updateBatchPrefix(scanner.getBatchPrefix());
    updatePagesToScan(new Integer(scanner.getPagesToScan()));
    updateBatchScanCheckBox(KnownSaneOptions.isBatchScanAvailable(scanner),
        KnownSaneOptions.isBatchScanSelected(scanner));

  }

  private void autoCropCheckBoxStateChanged(ActionEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    KnownSaneOptions.setAutoCrop(scanner, components.getAutoCropCheckBox().isSelected());
  }

  private void batchNameCaretUpdateActionPerformed(CaretEvent e) {
    ScannerListItem currentScannerListItem = components.getScannerList().getSelectedValue();
    if (currentScannerListItem == null) {
      return;
    }
    Scanner scanner = currentScannerListItem.getScanner();
    if (scanner == null) {
      return;
    }
    scanner.setBatchPrefix(components.getBatchNameTextField().getText());
  }

  private void blackThresholdSpinnerStateChanged(ChangeEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    KnownSaneOptions.setBlackThreshold(scanner, (Integer) components.getBlackThresholdSpinner()
        .getValue());
  }

  private void cancelDetection(ActionEvent e) {
    if ((discoveryJob != null) && discoveryJob.isActive()) {
      discoveryJob.cancel();
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          components.getDetectScannersButton().setEnabled(true);
          components.getCancelDetectScannersButton().setEnabled(false);
          components.getDetectScannersButton().requestFocus();
        }
      });
    }
  }

  private void cancelScan(ActionEvent e) {
    final ScannerListItem scannerListItem = components.getScannerList().getSelectedValue();
    if (scannerListItem == null) {
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          showMessageDialog(Localizer.localize("SelectAScannerMessage"));
        }
      });
      return;
    }
    if (scannerListItem.isActive()) {
      scannerListItem.cancel();
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          updateScannerListItem(scannerListItem, ScannerStatus.IDLE);
        }
      });
    }
  }

  private void colorComboBoxActionPerformed(ItemEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    KnownSaneOptions
        .setColorMode(scanner, (String) components.getColorComboBox().getSelectedItem());
  }

  private void customSettingsActionPerformed(ActionEvent e) {
    if (components.getScannerList().getSelectedIndex() < 0) {
      return;
    }
    ScannerListItem scannerListItem = components.getScannerList().getSelectedValue();
    Scanner scanner = scannerListItem.getScanner();
    CustomSettingsDialog customSettingsDialog = new CustomSettingsDialog(
        components.getRootComponent());
    customSettingsDialog.setXstream(xstream);
    customSettingsDialog.setScanner(scanner);
    customSettingsDialog.setScanService(scanService);
    customSettingsDialog.initialize();
    customSettingsDialog.setModal(true);
    customSettingsDialog.setVisible(true);
    if (customSettingsDialog.getDialogResult() == JOptionPane.OK_OPTION) {
      PreferencesUtils.update(scanner, customSettingsDialog.getScanner());
    }
  }

  private void defaultThresholdCheckBoxStateChanged(ActionEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    boolean isUsingDefaultBlackThreshold = components.getDefaultThresholdCheckBox().isSelected();
    KnownSaneOptions.setUsingDefaultBlackThreshold(scanner, isUsingDefaultBlackThreshold);
    components.getBlackThresholdSpinner().setEnabled(!isUsingDefaultBlackThreshold);
  }

  private void detectScanners() {
    discoveryJob = new DiscoveryJob(scanService);
    discoveryJob.setNotificaiton(this);
    discoveryListener = new DiscoveryListener() {
      @Override
      public void discoveryEventOccurred(final DiscoveryEvent devt) {
        SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            displayDevicesList(devt.getDiscoveredScanners());
            discoveryJob.removeDiscoveryListener(discoveryListener);
            components.getDetectScannersButton().setEnabled(true);
            components.getCancelDetectScannersButton().setEnabled(false);
            components.getDetectScannersButton().requestFocus();
            discoveryListener = null;
            discoveryJob = null;
          }
        });
      }
    };
    discoveryJob.addDiscoveryListener(discoveryListener);
    discoveryJob.discover();
  }

  private void detectScanners(String hostAddress, int portNumber, String description)
      throws IOException, SaneException {
    discoveryJob = new DiscoveryJob(scanService);
    discoveryJob.setNotificaiton(this);
    discoveryListener = new DiscoveryListener() {
      @Override
      public void discoveryEventOccurred(final DiscoveryEvent devt) {
        SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            displayDevicesList(devt.getDiscoveredScanners());
            discoveryJob.removeDiscoveryListener(discoveryListener);
            components.getDetectScannersButton().setEnabled(true);
            components.getCancelDetectScannersButton().setEnabled(false);
            components.getDetectScannersButton().requestFocus();
            discoveryListener = null;
            discoveryJob = null;
          }
        });
      }
    };
    discoveryJob.addDiscoveryListener(discoveryListener);
    discoveryJob.discover(InetAddress.getByName(hostAddress), portNumber, description);
  }

  private void detectScannersActionPerformed(ActionEvent e) {
    if ((discoveryJob == null) || !(discoveryJob.isActive())) {
      detectScanners();
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          components.getDetectScannersButton().setEnabled(false);
          components.getCancelDetectScannersButton().setEnabled(true);
          components.getCancelDetectScannersButton().requestFocus();
        }
      });
    }
  }

  private void disableScanButtons() {
    components.getScanProgressBar().setEnabled(false);
    components.getScanButton().setEnabled(false);
    components.getCancelScanButton().setEnabled(false);
  }

  private void disableSettingsComponents() {
    components.getSaveSettingsButton().setEnabled(false);
    components.getUseCustomSettingsCheckBox().setSelected(false);
    components.getUseCustomSettingsCheckBox().setEnabled(false);
    components.getCustomSettingsButton().setEnabled(false);
    updateResolutionModel(null, null);
    updatePageSizeModel(null, null);
    updateColorModeModel(null, null);
    updateDuplexCheckBox(false, false);
    updateAutoCropCheckBox(false, false);
    updateBatchScanCheckBox(false, false);
    updateBlackThresholdModel(null);
    updateUsingBlackThresholdCheckBox(false);
    updateSourceModel(null, null);
    updateBatchPrefix(null);
    updatePagesToScan(null);
  }

  /**
   * @param discoveredScanners
   *          an {@link ArrayList} of {@link Scanner}
   */
  private void displayDevicesList(final ArrayList<Scanner> discoveredScanners) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        updateScannerList(discoveredScanners);
      }
    });
  }

  private void duplexScanningCheckBoxStateChanged(ActionEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    KnownSaneOptions.setDuplex(scanner, components.getDuplexScanningCheckBox().isSelected());
  }

  private void editScannerActionPerformed(ActionEvent e) {
    if (components.getScannerList().getSelectedIndex() < 0) {
      return;
    }
    ScannerListItem scannerListItem = components.getScannerList().getSelectedValue();
    Scanner scanner = scannerListItem.getScanner();

    ScannerSettingsDialog scannerSettingsDialog = new ScannerSettingsDialog(
        components.getRootComponent());
    scannerSettingsDialog.setTitle(Localizer.localize("EditScannerDialogTitle"));
    scannerSettingsDialog.setRemoteAddress(scanner.getRemoteAddress());
    scannerSettingsDialog.setRemotePortNumber(new Integer(scanner.getRemotePortNumber()));
    scannerSettingsDialog.setDescription(scanner.getDescription());
    scannerSettingsDialog.setModal(true);
    scannerSettingsDialog.setVisible(true);
    if (scannerSettingsDialog.getDialogResult() == JOptionPane.OK_OPTION) {
      scanner.setDescription(scannerSettingsDialog.getDescription());
      scanner.setRemoteAddress(scannerSettingsDialog.getRemoteAddress());
      scanner.setRemotePortNumber(scannerSettingsDialog.getPortNumber());
      try {
        preferences.save();
      } catch (IOException ex) {
        LOG.error(ex, ex);
        Misc.showSuccessMsg(
            components.getRootComponent(),
            String.format(Localizer.localize("ScannerSettingsNotSavedMessage"),
                ex.getLocalizedMessage()));
      }
      components.getScannerList().revalidate();
      components.getScannerList().repaint();
    }
  }

  private void enableScanButtons() {
    components.getScanButton().setEnabled(true);
  }

  public final IComponents getComponents() {
    return components;
  }

  @Override
  public final Exception getException() {
    return null;
  }

  public final ISwingSanePreferences getPreferences() {
    return preferences;
  }

  public final IPreferredDefaults getPreferredDefaults() {
    return preferredDefaults;
  }

  public final IScanEventHandler getScanEventHandler() {
    return scanEventHandler;
  }

  public final IScanService getScanService() {
    return scanService;
  }

  private ScannerListItem getSelectedScannerListItem() {
    final ScannerListItem scannerListItem = components.getScannerList().getSelectedValue();
    if (scannerListItem == null) {
      return null;
    }
    return scannerListItem;
  }

  private String getTimeStamp() {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
    String timeString = dateFormat.format(new Date().getTime());
    return timeString;
  }

  public final XStream getXstream() {
    return xstream;
  }

  private void initComponentListeners() {

    JComboBox<String> sourceComboBox = components.getSourceComboBox();
    sourceComboBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        sourceComboBoxActionPerformed(e);
      }
    });

    JComboBox<Integer> resolutionComboBox = components.getResolutionComboBox();
    resolutionComboBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        resolutionComboBoxActionPerformed(e);
      }
    });

    JComboBox<String> colorComboBox = components.getColorComboBox();
    colorComboBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        colorComboBoxActionPerformed(e);
      }
    });

    JComboBox<String> pageSizeComboBox = components.getPageSizeComboBox();
    pageSizeComboBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        pageSizeComboBoxActionPerformed(e);
      }
    });

    JSpinner pagesToScanSpinner = components.getPagesToScanSpinner();
    pagesToScanSpinner.addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged(ChangeEvent evt) {
        pagesToScanSpinnerStateChanged(evt);
      }
    });

    JSpinner blackThresholdSpinner = components.getBlackThresholdSpinner();
    blackThresholdSpinner.addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged(ChangeEvent evt) {
        blackThresholdSpinnerStateChanged(evt);
      }
    });

    JCheckBox duplexScanningCheckBox = components.getDuplexScanningCheckBox();
    duplexScanningCheckBox.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        duplexScanningCheckBoxStateChanged(e);
      }
    });

    JCheckBox autoCropCheckBox = components.getAutoCropCheckBox();
    autoCropCheckBox.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        autoCropCheckBoxStateChanged(e);
      }
    });

    JCheckBox useADFCheckBox = components.getBatchScanCheckBox();
    useADFCheckBox.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        useADFCheckBoxStateChanged(e);
      }
    });

    JCheckBox defaultThresholdCheckBox = components.getDefaultThresholdCheckBox();
    defaultThresholdCheckBox.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        defaultThresholdCheckBoxStateChanged(e);
      }
    });

    JTextField batchNameTextField = components.getBatchNameTextField();
    batchNameTextField.addCaretListener(new CaretListener() {
      @Override
      public void caretUpdate(CaretEvent e) {
        batchNameCaretUpdateActionPerformed(e);
      }
    });

    JCheckBox useCustomSettingsCheckBox = components.getUseCustomSettingsCheckBox();
    useCustomSettingsCheckBox.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        useCustomSettingsActionPerformed(e);
      }
    });

    JButton customSettingsButton = components.getCustomSettingsButton();
    customSettingsButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        customSettingsActionPerformed(e);
      }
    });

    JButton addScannerButton = components.getAddScannerButton();
    addScannerButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        addScannerButtonActionPerformed(e);
      }
    });

    JButton globalSettingsButton = components.getGlobalSettingsButton();
    globalSettingsButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        showGlobalSettingsDialog();
      }
    });

    JButton saveSettingsButton = components.getSaveSettingsButton();
    saveSettingsButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        saveSettingsButtonActionPerformed(e);
      }
    });

    JButton editScannerButton = components.getEditScannerButton();
    editScannerButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        editScannerActionPerformed(e);
      }
    });

    JButton removeScannerButton = components.getRemoveScannerButton();
    removeScannerButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        removeScannerButtonActionPerformed(e);
      }
    });

    JButton scanButton = components.getScanButton();
    scanButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        scanActionPerformed(e);
      }
    });
    scanButton.setEnabled(false);

    JButton cancelScanButton = components.getCancelScanButton();
    cancelScanButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        cancelScan(e);
      }
    });
    cancelScanButton.setEnabled(false);

    JList<ScannerListItem> scannerList = components.getScannerList();
    scannerList.addListSelectionListener(new ListSelectionListener() {
      @Override
      public void valueChanged(ListSelectionEvent e) {
        scannerListSelectionChanged(e);
      }
    });

    JButton detectScannersButton = components.getDetectScannersButton();
    detectScannersButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        detectScannersActionPerformed(e);
      }
    });
    detectScannersButton.setEnabled(true);

    JButton detectCancelButton = components.getCancelDetectScannersButton();
    detectCancelButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        cancelDetection(e);
      }
    });
    detectCancelButton.setEnabled(false);

  }

  public final boolean isAddScannerConfirmed(Scanner scanner) {
    return Misc.showConfirmDialog(
        components.getRootComponent(),
        Localizer.localize("ConfirmAddScannerTitle"),
        String.format(Localizer.localize("ConfirmAddScannerMessage"), scanner.getName(),
            scanner.getRemoteAddress() + ":" + scanner.getRemotePortNumber()),
        Localizer.localize("OK"), Localizer.localize("Cancel"));
  }

  public final boolean isBusy() {
    if (scannerListModel == null) {
      return false;
    }
    for (int i = 0; i < scannerListModel.getSize(); i++) {
      ScannerListItem scannerListItem = scannerListModel.getElementAt(i);
      ScanEventListener[] scanListeners = scannerListItem.getListeners(ScanEventListener.class);
      if ((scanListeners != null) && (scanListeners.length > 0)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public final boolean isInterrupted() {
    return false;
  }

  private boolean isSameScanner(Scanner s1, Scanner s2) {
    if (s1.equals(s2)) {
      return true;
    }
    if (!(s1.getName().equals(s2.getName()))) {
      return false;
    }
    if (!(s1.getRemoteAddress().equals(s2.getRemoteAddress()))) {
      return false;
    }
    if (s1.getRemotePortNumber() != s2.getRemotePortNumber()) {
      return false;
    }
    return true;
  }

  @Override
  public final void message(final String message) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        addMessage(message);
      }
    });
  }

  private void pageSizeComboBoxActionPerformed(ItemEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    KnownSaneOptions.setScanArea(scanner, (String) components.getPageSizeComboBox()
        .getSelectedItem());
  }

  private void pagesToScanSpinnerStateChanged(ChangeEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    scanner.setPagesToScan((Integer) components.getPagesToScanSpinner().getValue());
  }

  private String parseBatchPrefix(String batchPrefix) {
    return batchPrefix.replace(Localizer.localize("TimeStampToken"), getTimeStamp());
  }

  private void removeScannerButtonActionPerformed(ActionEvent e) {

    ScannerListItem selectedScannerListItem = components.getScannerList().getSelectedValue();
    if (selectedScannerListItem == null) {
      return;
    }

    ApplicationPreferences applicationPreferences = preferences.getApplicationPreferences();
    ArrayList<Scanner> scannerList = applicationPreferences.getScannerList();
    ArrayList<Scanner> removalList = new ArrayList<Scanner>();

    if ((scannerList != null) && (scannerList.size() > 0)) {
      for (Scanner scanner : scannerList) {
        if (isSameScanner(scanner, selectedScannerListItem.getScanner())) {
          removalList.add(scanner);
        }
      }
    }

    for (Scanner scanner : removalList) {
      scannerList.remove(scanner);
    }

    scannerListModel.removeElement(selectedScannerListItem);

    disableSettingsComponents();

    try {
      preferences.save();
    } catch (IOException ex) {
      LOG.error(ex, ex);
    }

  }

  private void resolutionComboBoxActionPerformed(ItemEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    KnownSaneOptions.setResolution(scanner, (Integer) components.getResolutionComboBox()
        .getSelectedItem());
  }

  private void restorePreferences() {
    ApplicationPreferences applicationPreferences = preferences.getApplicationPreferences();
    ArrayList<Scanner> scannerList = applicationPreferences.getScannerList();
    if ((scannerList != null) && (scannerList.size() > 0)) {
      for (Scanner scanner : scannerList) {
        restorePreferencesFromScanner(scanner);
      }
    }
  }

  private void restorePreferencesFromScanner(Scanner scanner) {
    final ScannerListItem scannerListItem = new ScannerListItem(scanner);
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        scannerListModel.addElement(scannerListItem);
        components.getScannerList().revalidate();
        components.getScannerList().repaint();
      }
    });
  }

  private void savePreferences(boolean showSaveNotification) {
    savePreferences(showSaveNotification, null);
  }

  private void savePreferences(boolean showSaveNotification, ScannerListItem currentScannerListItem) {

    ApplicationPreferences applicationPreferences = preferences.getApplicationPreferences();

    ArrayList<Scanner> scannerList = applicationPreferences.getScannerList();

    if (currentScannerListItem == null) {
      Enumeration<ScannerListItem> elements = scannerListModel.elements();
      scannerList.clear();
      while (elements.hasMoreElements()) {
        ScannerListItem currentItem = elements.nextElement();
        scannerList.add(currentItem.getScanner());
      }
    } else {
      boolean updated = false;
      for (int i = 0; i < scannerList.size(); i++) {
        if (scannerList.get(i).getGuid().equals(currentScannerListItem.getScanner().getGuid())) {
          scannerList.set(i, currentScannerListItem.getScanner());
          updated = true;
        }
      }
      if (!updated) {
        scannerList.add(currentScannerListItem.getScanner());
      }
    }

    try {
      preferences.save();
      if (showSaveNotification) {
        Misc.showSuccessMsg(components.getRootComponent(),
            Localizer.localize("ScannerSettingsSavedSuccessfullyMessage"));
      }
    } catch (IOException ex) {
      LOG.error(ex, ex);
      Misc.showSuccessMsg(
          components.getRootComponent(),
          String.format(Localizer.localize("ScannerSettingsNotSavedMessage"),
              ex.getLocalizedMessage()));
    }

  }

  private void saveSettingsButtonActionPerformed(ActionEvent e) {
    ScannerListItem currentScannerListItem = components.getScannerList().getSelectedValue();
    savePreferences(true, currentScannerListItem);
  }

  private void scanActionPerformed(ActionEvent e) {
    final ScannerListItem scannerListItem = getSelectedScannerListItem();
    if (scannerListItem == null) {
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          showMessageDialog(Localizer.localize("SelectAScannerMessage"));
        }
      });
      return;
    }
    if (!(scannerListItem.isActive())) {
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          updateScannerListItem(scannerListItem, ScannerStatus.BUSY);
        }
      });
      ScanEventListener scanEventListener = new ScanEventListener() {
        @Override
        public void eventOccurred(final ScanEvent sevt) {
          if (sevt.getBufferedImage() == null) {
            SwingUtilities.invokeLater(new Runnable() {
              @Override
              public void run() {
                updateScannerListItem(scannerListItem, ScannerStatus.IDLE);
              }
            });
            scannerListItem.removeListeners();

            return;
          }
          scanEventHandler.scanPerformed(sevt);

        }
      };
      int pagesToScan = Integer.MAX_VALUE;
      if (components.getPagesToScanSpinner().isEnabled()) {
        pagesToScan = (Integer) components.getPagesToScanSpinner().getValue();
        if (components.getDuplexScanningCheckBox().isSelected()) {
          pagesToScan *= 2;
        }
      }
      ScanListenerAdapter progressBarUpdater = new ScanListenerAdapter() {
        @Override
        public void recordRead(SaneDevice device, final int totalBytesRead, final int imageSize) {
          final double fraction = (1.0d * totalBytesRead) / imageSize;
          SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
              if (components.getScannerList().getSelectedValue() == scannerListItem) {
                if (components.getScanProgressBar().isIndeterminate()) {
                  components.getScanProgressBar().setIndeterminate(false);
                }
                components.getScanProgressBar().setEnabled(true);
                components.getScanProgressBar().setValue((int) (fraction * 100));
              }
            }
          });
        }

        @Override
        public void scanningFinished(SaneDevice device) {
          SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
              if (components.getScannerList().getSelectedValue() == scannerListItem) {
                components.getScanProgressBar().setValue(0);
              }
            }
          });
        }
      };

      ScanListener rateLimitedScanListener = RateLimitingScanListeners.noMoreFrequentlyThan(
          progressBarUpdater, PROGRESS_BAR_UPDATE_INTERVAL, TimeUnit.MILLISECONDS);

      ScanJob scanJob = new ScanJob(scanService, scannerListItem.getScanner());
      scannerListItem.setScanEventListener(scanEventListener);
      scannerListItem.setScanJob(scanJob);
      scanJob.setPagesToScan(pagesToScan);
      scanJob.setUseADF(components.getBatchScanCheckBox().isSelected());
      scanJob.setBatchPrefix(parseBatchPrefix(scannerListItem.getScanner().getBatchPrefix()));
      scanJob.setNotificaiton(this);
      scanJob.addScanListener(scanEventListener);
      scanJob.acquire(rateLimitedScanListener);

    }
  }

  private void scannerListSelectionChanged(ListSelectionEvent e) {
    if (!(e.getValueIsAdjusting())) {
      return;
    }
    @SuppressWarnings("unchecked")
    JList<ScannerListItem> scannerList = (JList<ScannerListItem>) e.getSource();
    if (scannerList.getSelectedIndex() < 0) {
      disableSettingsComponents();
      disableScanButtons();
      return;
    }
    disableSettingsComponents();
    ScannerListItem scannerListItem = scannerList.getSelectedValue();
    Scanner scanner = scannerListItem.getScanner();
    adjustSettingsComponents(scanner);
    enableScanButtons();
    updateComponents(scannerListItem);
  }

  private void selectPreferredDefaults(Scanner scanner) {
    preferredDefaults.update(scanner);
  }

  public final void setComponents(IComponents components) {
    this.components = components;
    this.components.getScannerList().setModel(scannerListModel);
    initComponentListeners();
    restorePreferences();
  }

  @Override
  public void setException(Exception exception) {
  }

  @Override
  public void setInterrupted(boolean interrupted) {
  }

  public final void setPreferences(ISwingSanePreferences preferences) {
    this.preferences = preferences;
  }

  public final void setPreferredDefaults(IPreferredDefaults preferredDefaults) {
    this.preferredDefaults = preferredDefaults;
  }

  public final void setScanEventHandler(IScanEventHandler scanEventHandler) {
    this.scanEventHandler = scanEventHandler;
  }

  private void setScannerBusy(boolean scannerBusy) {
    if (scannerBusy) {
      components.getScanProgressBar().setIndeterminate(true);
      components.getScanProgressBar().setEnabled(true);
      components.getScanButton().setEnabled(false);
      components.getCancelScanButton().setEnabled(true);
      components.getCancelScanButton().requestFocus();
    } else {
      components.getScanProgressBar().setIndeterminate(false);
      components.getScanProgressBar().setValue(0);
      components.getScanProgressBar().setEnabled(false);
      components.getScanButton().setEnabled(true);
      components.getCancelScanButton().setEnabled(false);
      components.getScanButton().requestFocus();
    }
  }

  public final void setScanService(IScanService scanServiceImpl) {
    scanService = scanServiceImpl;
  }

  public final void setXstream(XStream xstream) {
    this.xstream = xstream;
  }

  private void showGlobalSettingsDialog() {
    HashMap<String, Login> saneLogins = PreferencesUtils.copy(preferences
        .getApplicationPreferences().getSaneLogins());
    String saneServiceName = preferences.getApplicationPreferences().getSaneServiceIdentity()
        .getServiceName();
    GlobalSettingsDialog globalSettingsDialog = new GlobalSettingsDialog(
        components.getRootComponent());
    globalSettingsDialog.setLogins(saneLogins);
    globalSettingsDialog.setSaneServiceName(saneServiceName);
    globalSettingsDialog.initialize();
    globalSettingsDialog.setModal(true);
    globalSettingsDialog.setVisible(true);
    if (globalSettingsDialog.getDialogResult() == JOptionPane.OK_OPTION) {
      try {
        preferences.getApplicationPreferences().getSaneServiceIdentity()
            .setServiceName(globalSettingsDialog.getServiceName().trim());
        PreferencesUtils.update(preferences.getApplicationPreferences().getSaneLogins(),
            globalSettingsDialog.getSaneLogins());
        preferences.save();
      } catch (IOException e) {
        LOG.error(e, e);
      }
    }
  }

  private void showMessageDialog(final String message) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        JOptionPane.showMessageDialog(components.getRootComponent(), message);
      }
    });
  }

  private void sourceComboBoxActionPerformed(ItemEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    KnownSaneOptions.setSource(scanner, (String) components.getSourceComboBox().getSelectedItem());
  }

  private void updateAutoCropCheckBox(boolean enabled, boolean selected) {
    JCheckBox autoCropCheckBox = components.getAutoCropCheckBox();
    autoCropCheckBox.setEnabled(enabled);
    autoCropCheckBox.setSelected(selected);
  }

  private void updateBatchPrefix(final String batchPrefix) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        JTextField batchNameTextField = components.getBatchNameTextField();
        if (batchPrefix == null) {
          batchNameTextField.setEnabled(false);
        } else {
          batchNameTextField.setEnabled(true);
        }
        batchNameTextField.setText(batchPrefix);
      }
    });
  }

  private void updateBatchScanCheckBox(boolean enabled, boolean selected) {
    JCheckBox batchScanCheckBox = components.getBatchScanCheckBox();
    batchScanCheckBox.setEnabled(enabled);
    batchScanCheckBox.setSelected(selected);
    if (enabled && selected) {
      components.getPagesToScanSpinner().setEnabled(false);
    } else {
      components.getPagesToScanSpinner().setEnabled(true);
    }
  }

  private void updateBlackThresholdModel(SpinnerModel blackThresholdSpinnerModel) {
    JSpinner blackThresholdSpinner = components.getBlackThresholdSpinner();
    blackThresholdSpinner.setModel(blackThresholdSpinnerModel != null ? blackThresholdSpinnerModel
        : new SpinnerNumberModel(1, 1, null, 1));
    blackThresholdSpinner.setEnabled(blackThresholdSpinnerModel != null ? !(components
        .getDefaultThresholdCheckBox().isSelected()) : false);
  }

  private void updateColorModeModel(ComboBoxModel<String> colorModeModel, String colorMode) {
    JComboBox<String> colorComboBox = components.getColorComboBox();
    colorComboBox.setModel(colorModeModel != null ? colorModeModel
        : new DefaultComboBoxModel<String>());
    colorComboBox.setEnabled(colorModeModel != null ? true : false);
    if (colorModeModel != null) {
      colorComboBox.setSelectedItem(colorMode);
    }
  }

  private void updateComponents(final ScannerListItem scannerListItem) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        if (components.getScannerList().getSelectedValue() == scannerListItem) {
          if (scannerListItem.getScannerStatus() == ScannerStatus.BUSY) {
            setScannerBusy(true);
          } else if (scannerListItem.getScannerStatus() == ScannerStatus.IDLE) {
            setScannerBusy(false);
          }
        }
        components.getScannerList().revalidate();
        components.getScannerList().repaint();
      }
    });
  }

  private void updateDuplexCheckBox(boolean enabled, boolean selected) {
    JCheckBox duplexCheckBox = components.getDuplexScanningCheckBox();
    duplexCheckBox.setEnabled(enabled);
    duplexCheckBox.setSelected(selected);
  }

  private void updatePageSizeModel(ComboBoxModel<String> pageSizeModel, String pageSize) {
    JComboBox<String> pageSizeComboBox = components.getPageSizeComboBox();
    pageSizeComboBox.setModel(pageSizeModel != null ? pageSizeModel
        : new DefaultComboBoxModel<String>());
    pageSizeComboBox.setEnabled(pageSizeModel != null ? true : false);
    if (pageSizeModel != null) {
      pageSizeComboBox.setSelectedItem(pageSize);
    }
  }

  private void updatePagesToScan(Integer pagesToScan) {
    JSpinner pagesToScanSpinner = components.getPagesToScanSpinner();
    if (pagesToScan == null) {
      pagesToScanSpinner.setEnabled(false);
    } else {
      pagesToScanSpinner.setEnabled(!(components.getBatchScanCheckBox().isSelected()));
      pagesToScanSpinner.setValue(pagesToScan);
    }
  }

  private void updateResolutionModel(ComboBoxModel<Integer> resolutionModel, Integer resolution) {
    JComboBox<Integer> resolutionComboBox = components.getResolutionComboBox();
    resolutionComboBox.setModel(resolutionModel != null ? resolutionModel
        : new DefaultComboBoxModel<Integer>());
    resolutionComboBox.setEnabled(resolutionModel != null ? true : false);
    if (resolutionModel != null) {
      resolutionComboBox.setSelectedItem(resolution);
    }
  }

  private void updateScannerList(ArrayList<Scanner> discoveredScanners) {
    if (discoveredScanners == null) {
      return;
    }
    for (Scanner scanner : discoveredScanners) {
      if (isAddScannerConfirmed(scanner)) {
        selectPreferredDefaults(scanner);
        scannerListModel.addElement(new ScannerListItem(scanner));
      }
    }
    components.getScannerList().setModel(scannerListModel);
    savePreferences(false);
  }

  private void updateScannerListItem(ScannerListItem scannerListItem, ScannerStatus scannerStatus) {
    scannerListItem.setScannerStatus(scannerStatus);
    updateComponents(scannerListItem);
  }

  private void updateSourceModel(ComboBoxModel<String> sourceModel, String source) {
    JComboBox<String> sourceComboBox = components.getSourceComboBox();
    sourceComboBox.setModel(sourceModel != null ? sourceModel : new DefaultComboBoxModel<String>());
    sourceComboBox.setEnabled(sourceModel != null ? true : false);
    if (source != null) {
      sourceComboBox.setSelectedItem(source);
    }
  }

  private void updateUsingBlackThresholdCheckBox(boolean usingDefaultBlackThreshold) {
    JCheckBox useDefaultBlackThreshold = components.getDefaultThresholdCheckBox();
    useDefaultBlackThreshold.setSelected(usingDefaultBlackThreshold);
  }

  private void useADFCheckBoxStateChanged(ActionEvent e) {
    if (getSelectedScannerListItem() == null) {
      return;
    }
    Scanner scanner = getSelectedScannerListItem().getScanner();
    if (scanner == null) {
      return;
    }
    boolean isBatchScanSelected = components.getBatchScanCheckBox().isSelected();
    KnownSaneOptions.setBatchScan(scanner, isBatchScanSelected);
    components.getPagesToScanSpinner().setEnabled(!isBatchScanSelected);
  }

  private void useCustomSettingsActionPerformed(ActionEvent e) {
    if (components.getUseCustomSettingsCheckBox().isSelected()) {
      components.getCustomSettingsButton().setEnabled(true);
      updateResolutionModel(null, null);
      updatePageSizeModel(null, null);
      updateColorModeModel(null, null);
      updateDuplexCheckBox(false, false);
      updateAutoCropCheckBox(false, false);
      updateBlackThresholdModel(null);
      updateUsingBlackThresholdCheckBox(false);
      updateSourceModel(null, null);
      if (components.getScannerList().getSelectedIndex() >= 0) {
        ScannerListItem scannerListItem = components.getScannerList().getSelectedValue();
        Scanner scanner = scannerListItem.getScanner();
        scanner.setUsingCustomOptions(true);
      }
    } else {
      if (components.getScannerList().getSelectedIndex() < 0) {
        disableSettingsComponents();
        return;
      }
      ScannerListItem scannerListItem = components.getScannerList().getSelectedValue();
      Scanner scanner = scannerListItem.getScanner();
      scanner.setUsingCustomOptions(false);
      adjustSettingsComponents(scanner);
    }
  }

}