/*
 * Copyright 2009 Lari Natri <lari.natri@gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package compid.tools;


import compid.importers.*;
import compid.data.DAOException;
import compid.data.tuples.ProtSeq;
import compid.data.tuples.Protein;
import net.aavalla.utils.jobpool.ParameterizedJob;
import net.aavalla.utils.jobpool.parameters.FieldAnnotations;
import net.aavalla.utils.jobpool.parameters.ParameterCollection;
import net.aavalla.utils.general.FileUtils;
import compid.importers.ImportException.ExceptionType;
import net.aavalla.utils.jobpool.parameters.InputFile;
import net.aavalla.utils.jobpool.parameters.validators.InputFileValidator;
import net.aavalla.utils.jobpool.parameters.OutputFile;
import net.aavalla.utils.jobpool.parameters.validators.OutputFileValidator;
import compid.tools.FASTARandomizer.FASTARandomizerParams;
import net.aavalla.utils.general.FastaUtils;
import net.aavalla.utils.general.FastaUtils.FastaLineType;
import net.aavalla.utils.general.StrUtils;
import net.aavalla.utils.jobpool.JobCategory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

/**
 *
 * @author Lari Natri
 */
public class FASTARandomizer extends ParameterizedJob<Object,FASTARandomizerParams> {

    public enum RandomizerType { Reverse, Random };



    public class FASTARandomizerParams extends ParameterCollection {

        @FieldAnnotations(name="Input file",description="Protein sequence database in FASTA format",validator=InputFileValidator.class)
        private InputFile sourceFile;

        @FieldAnnotations(name="Output file",description="Output file (FASTA), will be overwritten",validator=OutputFileValidator.class)
        private OutputFile destinationFile;

        @FieldAnnotations(name="AA Reordering method",description="Reverse or Randomize protein sequences")
        private RandomizerType reorderType = RandomizerType.Reverse;

        @FieldAnnotations(name="Keep accessions",description="If selected, original accessions will be kept (not recommended!). <br/>Otherwise they'll be prefixed.")
        private boolean keepAccessions = false;

        @FieldAnnotations(name="Include original sequences",description="If selected, original sequences will be included in the output file.")
        private boolean append = false;

        public FASTARandomizerParams() {
            this.setDescription("Creates a decoy database from given protein database<br/>" +
                                "by randomizing or reversing the sequences.");
            this.setHelpID("decoy");
        }

        public File getSourceFile() { return this.sourceFile; }
        public File getDestinationFile() { return this.destinationFile; }
        public RandomizerType getReorderType() { return this.reorderType; }
        public boolean getKeepAccessions() { return this.keepAccessions; }

        @Override
        public synchronized String validateFieldRelations() {
            if (this.keepAccessions && this.append) {
                return "Cannot keep old accessions AND keep original sequences";
            } else {
                return super.validateFieldRelations();
            }
        }
    } // class


    private static final double MINAARATIOINSEQUENCE = 0.95d;
    private static final int MAXERRONEUSSEQUENCES = 500;

    public FASTARandomizer() {
        super();
        this.addCategory(JobCategory.FILESYSTEM);
        this.setName("Decoy Protein Database creator (FASTA)");
    }



    private static final String PREFIX_RND = "RND-";
    private static final String PREFIX_REV = "REV-";
    private static final String PREFIX_RND_DESC = "Randomized, was ";
    private static final String PREFIX_REV_DESC = "Reversed, was ";


    @Override
    public FASTARandomizerParams getNonfinalEditableParameters() {
        return new FASTARandomizerParams();
    }

    @Override
    public Object work(FASTARandomizerParams params) {

        FileReader fileReader = null;
        BufferedReader bufReader = null;
        FileWriter fileWriter = null;
        BufferedWriter bufWriter = null;
        int erroneusSequencesCount = 0;
        String prefix = params.getReorderType().equals(RandomizerType.Reverse) ? PREFIX_REV : PREFIX_RND;
        String prefixDesc = params.getReorderType().equals(RandomizerType.Reverse) ? PREFIX_REV_DESC : PREFIX_RND_DESC;
        if (params.getKeepAccessions()) {
            prefix = "";
        }

        if (!params.getSourceFile().exists())
            throw new ImportException(ExceptionType.FILENOTFOUND,
                                      params.getSourceFile().toString());
        if (!params.getSourceFile().canRead())
            throw new ImportException(ExceptionType.FILENOTREADABLE,
                                      params.getSourceFile().toString());
        
        FileUtils.EOLStyle eolStyle = FileUtils.EOLStyle.UNKNOWN;
        try {
            eolStyle = FileUtils.getFileEOLStyle(params.getSourceFile());
        } catch (FileNotFoundException ex) {
            throw new ImportException(ExceptionType.FILENOTFOUND,
                                      params.getSourceFile().toString());
        } catch (IOException ioex) {
            throw new ImportException(ExceptionType.FILENOTREADABLE,
                                      params.getSourceFile().toString());
        }
        if (eolStyle.equals(FileUtils.EOLStyle.UNKNOWN)) {
            eolStyle = FileUtils.EOLStyle.SYSTEM;
        }

        long fileSize = params.getSourceFile().length();
        long curChars = 0;


        try {
            fileReader = new FileReader(params.getSourceFile());
            bufReader = new BufferedReader(fileReader, FASTAImporter.FILEBUFFERSIZE);
            fileWriter = new FileWriter(params.getDestinationFile());
            bufWriter = new BufferedWriter(fileWriter, FASTAImporter.FILEBUFFERSIZE);
            String nextFullHeaderLine;

            do { // read file until first header is found
                nextFullHeaderLine = bufReader.readLine();
                if (nextFullHeaderLine==null) { // EOF reached
                    throw new ImportException(ExceptionType.FASTA_INCORRECTFORMAT,
                            "File "+params.getSourceFile().getName()+" is not in " +
                            "FASTA format. (Didn't find any headers.)");
                }
                curChars+=nextFullHeaderLine.length();
                nextFullHeaderLine = nextFullHeaderLine.trim();
            } while (FastaUtils.getFastaLineType(nextFullHeaderLine)!=FastaLineType.HEADER);

            while (nextFullHeaderLine!=null) {

                this.reportProgress((int)Math.round(100*curChars/(double)fileSize));

                boolean entryComplete = false;
                StringBuilder sequenceBuilder = new StringBuilder();
                String fullHeaderLine = nextFullHeaderLine;

                while (!entryComplete) { // read lines until entry is complete
                    String line = bufReader.readLine();
                    if (line==null) { // EOF reached
                        entryComplete = true;
                        nextFullHeaderLine = null; // to signify EOF
                    } else { // a line was read
                        curChars+=line.length();
                        line = line.trim();
                        switch(FastaUtils.getFastaLineType(line)) {
                            case HEADER:
                                nextFullHeaderLine = line;
                                entryComplete = true;
                                break;
                            case SEQUENCE:
                                sequenceBuilder.append(line);
                                break;
                            case COMMENT:
                            case OTHER:
                            default:
                                break;
                        }
                    } // else
                }

                ProtSeq protSeq = new ProtSeq(sequenceBuilder.toString(), true);
                double ratioOfAA = ((double)protSeq.getLength()) / ((double)sequenceBuilder.length());
                if (ratioOfAA < MINAARATIOINSEQUENCE) { // too many non-AA symbols in sequence
                    System.out.println("DEBUG: sequence has "+(sequenceBuilder.length()-protSeq.getLength())+" non-AA symbols (they were removed)");
                    System.out.println("       remaining size: "+protSeq.getLength());
                    
                    ++erroneusSequencesCount;
                    if (erroneusSequencesCount > MAXERRONEUSSEQUENCES) {
                        throw new ImportException(ExceptionType.FASTA_INCORRECTFORMAT,
                                "File "+params.getSourceFile().getName()+" is not in " +
                                "FASTA format. (Too many erroneus sequences.)");
                    }
                }

                String[] headers = fullHeaderLine.split("\u0001"); // split by Ctrl-A
                headers[0] = headers[0].substring(1); // remove '>' from beginning

                ArrayList<Protein> protsSharingSeq = new ArrayList<Protein>();

                for (int h = 0; h < headers.length; ++h) { // loop through headers for the same sequence
                    String descr = "";
                    String idFull = "";

                    String[] idAndDescrParts = headers[h].split("\\s",2); // split by first whitespace
                    if (idAndDescrParts.length >= 1) {
                        idFull = idAndDescrParts[0];
                        if (idAndDescrParts.length >= 2) {
                            descr = idAndDescrParts[1];
                        }
                    }

                    int prefixPos = idFull.indexOf('|')+1;

                    if (prefixPos >= 2 && prefixPos <= 4) {
                        while (prefixPos<idFull.length() && idFull.charAt(prefixPos)=='|') {
                            ++prefixPos;
                        }
                    } else {
                        prefixPos = 0;
                    }
                    String idFixed = idFull.substring(0,prefixPos) +
                            prefix + idFull.substring(prefixPos,idFull.length());
                    protsSharingSeq.add(new Protein(idFixed,prefixDesc+descr,"<tmp>"));

                } // for headers

                // add original sequence if so requested
                if (params.append) {
                    bufWriter.write(fullHeaderLine + eolStyle.EOL());
                    String[] splitOrigSeq = StrUtils.slice(sequenceBuilder.toString(), 70);
                    for (int i=0; i < splitOrigSeq.length; ++i) {
                        bufWriter.write(splitOrigSeq[i] + eolStyle.EOL());
                    }
                }

                bufWriter.write(">");

                for (int i=0; i<protsSharingSeq.size(); ++i) {
                    Protein p = protsSharingSeq.get(i);
                    if (i>0) bufWriter.write('\u0001'); // CTRL-A to separate different accessions for the same sequence
                    bufWriter.write(p.getAccession()+" "+p.getDescription());
                }

                bufWriter.write(eolStyle.EOL());

                String randSeq;
                if (params.getReorderType().equals(RandomizerType.Reverse)) {
                    randSeq = protSeq.getSequence().componentsInReversedOrder().toString();
                } else {
                    randSeq = protSeq.getSequence().componentsInRandomOrder().toString();
                }

                String[] splitSeq = StrUtils.slice(randSeq, 70);

                for (int i=0; i < splitSeq.length; ++i) {
                    bufWriter.write(splitSeq[i]+eolStyle.EOL());
                }


            } // end: while there is something to read in the FASTA file

            System.out.println("DEBUG: There were "+erroneusSequencesCount+" erroneus sequences");
            return null;


        } catch (FileNotFoundException ex) {
            throw new ImportException(ExceptionType.FILENOTFOUND,
                                      params.getSourceFile().getAbsolutePath());
        } catch (IOException ex) {
            throw new ImportException(ex);
        } catch (DAOException ex) {
            throw new ImportException(ex);
        } finally {
            try {
                bufWriter.close();
                fileWriter.close();
                bufReader.close();
                fileReader.close();
            } catch (Exception ex) {} // never mind this
        }

    }


}
