package au.com.southsky.jfreesane;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.io.CharSource;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Represents the authentication configuration used by SANE clients. The SANE utilities like
 * {@code scanimage} will read the {@code ~/.sane/pass} directory (if it exists), this class
 * provides an implementation of that behavior.
 *
 * <p>
 * Threadsafe.
 */
public class SaneClientAuthentication extends SanePasswordProvider {
  private static final Logger logger = Logger.getLogger(SaneClientAuthentication.class.getName());

  public static final String MARKER_MD5 = "$MD5$";

  private static final String DEFAULT_CONFIGURATION_PATH =
      Joiner.on(File.separator).join(System.getProperty("user.home"), ".sane", "pass");

  private final Table<String, String, String> credentials = HashBasedTable.create();
  private final CharSource configurationSource;
  private boolean initialized = false;

  public SaneClientAuthentication() {
    this(DEFAULT_CONFIGURATION_PATH);
  }

  public SaneClientAuthentication(final String path) {
    this(
        new CharSource() {
          @Override
          public Reader openStream() throws IOException {
            return new InputStreamReader(new FileInputStream(path), StandardCharsets.US_ASCII);
          }
        });
  }

  /**
   * Returns a new {@code SaneClientAuthentication} whose configuration is represented by the
   * characters supplied by the given {@link CharSource}.
   */
  public SaneClientAuthentication(CharSource configurationSource) {
    this.configurationSource = configurationSource;
  }

  private synchronized void initializeIfRequired() {
    if (initialized) {
      return;
    }

    initialized = true;
    try (BufferedReader r = configurationSource.openBufferedStream()) {
      String line;

      int lineNumber = 0;
      while ((line = r.readLine()) != null) {
        lineNumber++;
        ClientCredential credential = ClientCredential.fromAuthString(line);
        if (credential == null) {
          logger.log(
                  Level.WARNING,
                  "ignoring invalid configuration format (line {0}): {1}",
                  new Object[]{lineNumber, line});
        } else {
          credentials.put(credential.backend, credential.username, credential.password);
          if (credentials.row(credential.backend).size() > 1) {
            logger.log(
                    Level.WARNING,
                    "ignoring line {0}, we already have a configuration for resource [{1}]",
                    new Object[]{lineNumber, credential.backend});
          }
        }
      }
    } catch (IOException e) {
      logger.log(Level.WARNING, "could not read auth configuration due to IOException", e);
    }
  }

  /**
   * Returns {@code true} if the configuration contains an entry for the given resource.
   */
  @Override
  public boolean canAuthenticate(String resource) {
    if (resource == null) {
      return false;
    }

    ClientCredential credential = getCredentialForResource(resource);
    return credential != null;
  }

  public ClientCredential getCredentialForResource(String rc) {
    initializeIfRequired();
    String resource = rc.contains(MARKER_MD5) ? rc.substring(0, rc.indexOf(MARKER_MD5)) : rc;

    Map<String, String> credentialsForResource = credentials.row(resource);
    for (Map.Entry<String, String> credential : credentialsForResource.entrySet()) {
      return new ClientCredential(resource, credential.getKey(), credential.getValue());
    }

    return null;
  }

  @Override
  public String getUsername(String resource) {
    return getCredentialForResource(resource).username;
  }

  @Override
  public String getPassword(String resource) {
    return getCredentialForResource(resource).password;
  }

  /**
   * Class to hold Sane client credentials organised by backend.
   *
   * @author paul
   */
  public static class ClientCredential {
    private final String backend;
    private final String username;
    private final String password;

    protected ClientCredential(String backend, String username, String password) {
      this.backend = backend;
      this.username = username;
      this.password = password;
    }

    public static ClientCredential fromAuthString(String authString) {
      List<String> fields = Splitter.on(":").splitToList(authString);
      if (fields.size() < 3) {
        return null;
      }

      return new ClientCredential(fields.get(2), fields.get(0), fields.get(1));
    }

    public String getBackend() {
      return backend;
    }

    public String getUsername() {
      return username;
    }

    public String getPassword() {
      return password;
    }
  }
}

@@ -111,97 +111,8 @@
 er;%0A
-import com.google.common.collect.HashBasedTable;%0Aimport com.google.common.collect.Table;%0A
 impo
@@ -359,16 +359,44 @@
 arsets;%0A
+import java.util.ArrayList;%0A
 import j
@@ -414,30 +414,8 @@
 st;%0A
-import java.util.Map;%0A
 impo
@@ -1132,76 +1132,112 @@
 nal 
-Table
+List
 %3CString
-, String, String%3E credentials = HashBasedTable.create();
+%3E resources;%0A  private final List%3CString%3E usernames;%0A  private final List%3CString%3E passwords;%0A
 %0A  p
@@ -1981,16 +1981,139 @@
 Source;%0A
+    this.resources = new ArrayList%3C%3E(4);%0A    this.usernames = new ArrayList%3C%3E(4);%0A    this.passwords = new ArrayList%3C%3E(4);%0A
   %7D%0A%0A  p
@@ -2555,36 +2555,32 @@
 (%0A              
-    
 Level.WARNING,%0A 
@@ -2570,36 +2570,32 @@
  Level.WARNING,%0A
-    
               %22i
@@ -2655,36 +2655,32 @@
 ,%0A              
-   
- 
 new Object%5B%5D%7Blin
@@ -2667,32 +2667,33 @@
     new Object%5B%5D
+ 
 %7BlineNumber, lin
@@ -2728,381 +2728,134 @@
     
-c
 re
-dentials.put(credential.backend, credential.username, credential.password);%0A          if (credentials.row(credential.backend).size() %3E 1) %7B%0A            logger.log(%0A                    Level.WARNING,%0A                    %22ignoring line %7B0%7D, we already have a configuration for resource %5B%7B1%7D%5D%22,%0A                    new Object%5B%5D%7BlineNumber, credential.backend%7D);%0A          %7D
+sources.add(credential.backend);%0A          usernames.add(credential.username);%0A          passwords.add(credential.password);
 %0A   
@@ -3523,250 +3523,159 @@
     
-Map%3CString, String%3E credentialsForResource = credentials.row(resource);%0A    for (Map.Entry%3CString, String%3E credential : credentialsForResource.entrySet()) %7B%0A      return new ClientCredential(resource, credential.getKey(), credential.getValue(
+int idx = resources.indexOf(resource);%0A    if (idx != -1) %7B%0A      return new ClientCredential(resources.get(idx), usernames.get(idx), passwords.get(idx
 ));%0A

Replace HashBasedTable with three lists.