/*
 *                    BioJava development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  If you do not have a copy,
 * see:
 *
 *      http://www.gnu.org/copyleft/lesser.html
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 * Created on 01-21-2010
 */
package org.biojava3.core.sequence.io.util;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

import org.biojava3.core.exceptions.ParserException;
import org.biojava3.core.sequence.compound.AmbiguityDNACompoundSet;
import org.biojava3.core.sequence.compound.AmbiguityRNACompoundSet;
import org.biojava3.core.sequence.compound.DNACompoundSet;
import org.biojava3.core.sequence.compound.RNACompoundSet;
import org.biojava3.core.sequence.template.Compound;
import org.biojava3.core.sequence.template.CompoundSet;
import org.biojava3.core.sequence.template.Sequence;

public class IOUtils {

  private static final int BUFFER = 4096;

  /**
   * Closes any Object which implements the interface {@link Closeable} and
   * sending any error to the logger but not forcing any explicit catching of
   * stream errors.
   *
   * @param c The stream to close
   */
  public static void close(Closeable c) {
    try {
      if (c != null) {
        c.close();
      }
    } catch (IOException e) {
      Logger log = Logger.getLogger(IOUtils.class.getName());
      log.log(Level.WARNING, "Cannot close down the given Closeable object", e);
    }
  }

  /**
   * Moves the bytes from input to output using a 4KB byte array.
   *
   * @param input Input stream of bytes
   * @param output Output stream of bytes
   * @throws IOException If anything occurs in the case of the reads and writes
   */
  public static void copy(InputStream input, OutputStream output)
      throws IOException {
    byte[] buffer = new byte[BUFFER];
    int n = 0;
    while (-1 != (n = input.read(buffer))) {
      output.write(buffer, 0, n);
    }
  }

  /**
   * Takes in a reader and a processor, reads every line from the given
   * file and then invokes the processor. What you do with the lines is
   * dependent on your processor.
   *
   * The code will automatically close the given BufferedReader.
   *
   * @param br The reader to process
   * @param processor The processor to invoke on all lines
   * @throws ParserException Can throw this if we cannot parse the given reader
   */
  public static void processReader(BufferedReader br, ReaderProcessor processor) throws ParserException {
    String line;
    try {
      while( (line = br.readLine()) != null ) {
        processor.process(line);
      }
    }
    catch(IOException e) {
      throw new ParserException("Could not read from the given BufferedReader");
    }
    finally {
      close(br);
    }
  }

  /**
   * Returns the contents of a buffered reader as a list of strings
   *
   * @param br BufferedReader to read from; <strong>will be closed</strong>
   * @return List of Strings
   * @throws ParserException Can throw this if we cannot parse the given reader
   */
  public static List<String> getList(BufferedReader br) throws ParserException {
    final List<String> list = new ArrayList<String>();
    processReader(br, new ReaderProcessor() {
      public void process(String line) {
        list.add(line);
      }
    });
    return list;
  }

  /**
   * Delegates to {@link #getList(BufferedReader)} by wrapping the InputStream
   * in a valid reader. No encoding is mentioned so if you need anything
   * more advanced then use the other version of this method.
   *
   * @param is InputStream which is a text file
   * @return List of Strings representing the lines of the files
   * @throws ParserException Can throw this if the file is not a file or we
   * cannot parse it
   */
  public static List<String> getList(InputStream is) throws ParserException {
    return getList(new BufferedReader(new InputStreamReader(is)));
  }

  /**
   * Delegates to {@link #getList(InputStream)} by wrapping the File
   * in a valid stream. No encoding is mentioned so if you need anything
   * more advanced then use the other version of this method. Since this
   * uses {@link #openFile(File)} this code can support GZipped and plain
   * files.
   *
   * @param file File which is a text file
   * @return List of Strings representing the lines of the files
   * @throws ParserException Can throw this if the file is not a file or we
   * cannot parse it
   */
  public static List<String> getList(File file) throws ParserException {
    return getList(openFile(file));
  }

  /**
   * For a filename this code will check the extension of the file for a
   * .gz extension. If it finds one then the InputStream given back
   * is a {@link GZIPInputStream}. Otherwise we return a normal
   * {@link FileInputStream}.
   *
   * @param file File which may or may not be GZipped
   * @return The final stream
   * @throws ParserException Can throw this if the file is not a file or we
   * cannot open it for processing
   */
  public static InputStream openFile(File file) throws ParserException {
    final InputStream is;
    if(!file.isFile()) {
      throw new ParserException("The file "+file+" is not a file.");
    }
    String name = file.getName();
    try {
      if(name.endsWith(".gz")) {
        is = new GZIPInputStream(new FileInputStream(file));
      }
      else {
        is = new FileInputStream(file);
      }
    }
    catch(IOException e) {
      throw new ParserException("Cannot open "+file+" for processing", e);
    }
    return is;
  }

  /**
   * Closure interface used when working with
   * {@link IOUtils#processReader(String)}. Each time a line is encountered
   * the object that implements this interface will be invoked.
   *
   * @author ayates
   */
  public static interface ReaderProcessor {
    void process(String line) throws IOException;
  }

  /**
   * Calculates GCG checksum for entire list of sequences
   *
   * @param sequences list of sequences
   * @return GCG checksum
   */
  public static <S extends Sequence<C>, C extends Compound> int getGCGChecksum(List<S> sequences) {
      int check = 0;
      for (S as : sequences) {
          check += getGCGChecksum(as);
      }
      return check % 10000;
  }

  /**
   * Calculates GCG checksum for a given sequence
   *
   * @param sequence given sequence
   * @return GCG checksum
   */
  public static <S extends Sequence<C>, C extends Compound> int getGCGChecksum(S sequence) {
      String s = sequence.toString().toUpperCase();
      int count = 0, check = 0;
      for (int i = 0; i < s.length(); i++) {
          count++;
          check += count * s.charAt(i);
          if (count == 57) {
              count = 0;
          }
      }
      return check % 10000;
  }

  /**
   * Assembles a GCG file header
   *
   * @param sequences list of sequences
   * @return GCG header
   */
  public static <S extends Sequence<C>, C extends Compound> String getGCGHeader(List<S> sequences) {
      StringBuilder header = new StringBuilder();
      S s1 = sequences.get(0);
      header.append(String.format("MSA from BioJava%n%n MSF: %d  Type: %s  Check: %d ..%n%n",
              s1.getLength(), getGCGType(s1.getCompoundSet()), getGCGChecksum(sequences)));
      String format = " Name: " + getIDFormat(sequences) + " Len: " + s1.getLength() + "  Check: %4d  Weight: 1.0%n";
      for (S as : sequences) {
          header.append(String.format(format, as.getAccession(), getGCGChecksum(as)));
          // TODO show weights in MSF header
      }
      header.append(String.format("%n//%n%n"));
      // TODO? convert gap characters to '.'
      return header.toString();
  }

  /**
   * Determines GCG type
   * @param cs compound set of sequences
   * @return GCG type
   */
  public static <C extends Compound> String getGCGType(CompoundSet<C> cs) {
      return (cs == DNACompoundSet.getDNACompoundSet() || cs == AmbiguityDNACompoundSet.getDNACompoundSet()) ? "D" :
          (cs == RNACompoundSet.getRNACompoundSet() || cs == AmbiguityRNACompoundSet.getRNACompoundSet()) ? "R" : "P";
  }

  /**
   * Creates format String for accession IDs
   *
   * @param sequences list of sequences
   * @return format String for accession IDs
   */
  public static <S extends Sequence<C>, C extends Compound> String getIDFormat(List<S> sequences) {
      int length = 0;
      for (S as : sequences) {
          length = Math.max(length, (as.getAccession() == null) ? 0 : as.getAccession().toString().length());
      }
      return (length == 0) ? null : "%-" + (length + 1) + "s";
  }

  /**
   * Creates formatted String for a single character of PDB output
   *
   * @param web true for HTML display
   * @param c1 character in first sequence
   * @param c2 character in second sequence
   * @param similar true if c1 and c2 are considered similar compounds
   * @param c character to display
   * @return formatted String
   */
  public static String getPDBCharacter(boolean web, char c1, char c2, boolean similar, char c) {
      String s = c + "";
      return getPDBString(web, c1, c2, similar, s, s, s, s);
  }

  /**
   * Creates formatted String for displaying conservation in PDB output
   *
   * @param web true for HTML display
   * @param c1 character in first sequence
   * @param c2 character in second sequence
   * @param similar true if c1 and c2 are considered similar compounds
   * @return formatted String
   */
  public static String getPDBConservation(boolean web, char c1, char c2, boolean similar) {
      return getPDBString(web, c1, c2, similar, "|", ".", " ", web ? "&nbsp;" : " ");
  }

  // helper method for getPDBCharacter and getPDBConservation
  private static String getPDBString(boolean web, char c1, char c2, boolean similar, String m, String sm, String dm,
          String qg) {
      if (c1 == c2)
          return web ? "<span class=\"m\">" + m + "</span>" : m;                             
      else if (similar)
          return web ? "<span class=\"sm\">" + sm + "</span>" : sm;
      else if (c1 == '-' || c2 == '-')
          return web ? "<span class=\"dm\">" + dm + "</span>" : dm;
      else
          return web ? "<span class=\"qg\">" + qg + "</span>" : qg;
  }

  /**
   * Creates formatted String for displaying conservation legend in PDB output
   *
   * @return legend String
   */
  public static String getPDBLegend() {
      StringBuilder s = new StringBuilder();
      s.append("</pre></div>");
      s.append("          <div class=\"subText\">");
      s.append("          <b>Legend:</b>");
      s.append("          <span class=\"m\">Green</span> - identical residues |"); 
      s.append("          <span class=\"sm\">Pink</span> - similar residues | ");
      s.append("          <span class=\"qg\">Blue</span> - sequence mismatch |");
      s.append("          <span class=\"dm\">Brown</span> - insertion/deletion |");                  
      s.append("      </div>");
      s.append(String.format("%n"));
      return s.toString();
  }

}
