/*************************************************************************\
 * AdapterRemoval - cleaning next-generation sequencing reads            *
 *                                                                       *
 * Copyright (C) 2011 by Stinus Lindgreen - stinus@binf.ku.dk            *
 * Copyright (C) 2014 by Mikkel Schubert - mikkelsch@gmail.com           *
 *                                                                       *
 * If you use the program, please cite the paper:                        *
 * S. Lindgreen (2012): AdapterRemoval: Easy Cleaning of Next Generation *
 * Sequencing Reads, BMC Research Notes, 5:337                           *
 * http://www.biomedcentral.com/1756-0500/5/337/                         *
 *                                                                       *
 * 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/>. *
\*************************************************************************/
#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <limits>
#include <sstream>
#include <iostream>

#include "debug.hpp"
#include "fastq_enc.hpp"


namespace ar
{

///////////////////////////////////////////////////////////////////////////////
// fastq_error

fastq_error::fastq_error(const std::string& message)
    : std::exception()
    , m_message(message)
{
}


fastq_error::fastq_error(const fastq_error& error)
    : std::exception()
    , m_message(error.m_message)
{
}


fastq_error::~fastq_error() noexcept
{
}


const char* fastq_error::what() const noexcept
{
    return m_message.c_str();
}


///////////////////////////////////////////////////////////////////////////////
// Pre-calculation of Solexa <-> Phred conversions

std::string calc_solexa_to_phred()
{
    std::string scores;
    scores.resize(MAX_PHRED_SCORE - MIN_SOLEXA_SCORE + 1);

    for (int i = MIN_SOLEXA_SCORE; i <= MAX_PHRED_SCORE; ++i) {
        const double score = round(10.0 * log10(1.0 + pow(10, (i / 10.0))));
        const int transformed = std::max<int>(MIN_PHRED_SCORE, std::min<int>(MAX_PHRED_SCORE, score));
        scores.at(i - MIN_SOLEXA_SCORE) = transformed;
    }

    return scores;
}


std::string calc_phred_to_solexa()
{
    std::string scores;
    scores.resize(MAX_PHRED_SCORE - MIN_PHRED_SCORE + 1);

    for (int i = MIN_PHRED_SCORE; i <= MAX_PHRED_SCORE; ++i) {
        const auto min_i = std::max(1, i);
        const auto score = round(10.0 * log10(pow(10.0, min_i / 10.0) - 1.0));
        const auto transformed = std::max<int>(MIN_SOLEXA_SCORE, std::min<int>(MAX_PHRED_SCORE, score));

        scores.at(i) = transformed;
    }

    return scores;
}


const std::string g_solexa_to_phred = calc_solexa_to_phred();
const std::string g_phred_to_solexa = calc_phred_to_solexa();


///////////////////////////////////////////////////////////////////////////////

[[noreturn]] void invalid_phred(const char offset, const char max_score, const char raw)
{
    if (raw < offset) {
        AR_DEBUG_ASSERT(offset == 33 || offset == 64);

        if (offset == 33) {
            throw fastq_error("ASCII value of quality score is less than 33 "
                              "(ASCII < '!'); input is corrupt or not in "
                              "FASTQ format!");
        } else if (offset == 64) {
            if (raw < ';') {
                throw fastq_error("Phred+64 encoded quality score is less than 0 "
                                  "(ASCII < ';'); Are these FASTQ reads actually in "
                                  "Phred+33 format? If so, use the command-line "
                                  "option \"--qualitybase 33\"\n\n"

                                  "See README for more information.");
            } else if (raw < '@') {
                // Value not less than -5, which is the lowest Solexa score
                throw fastq_error("Phred+64 encoded quality score is less than 0 "
                                  "(ASCII < '@'); Are these FASTQ reads actually in "
                                  "Phred+33 or Solexa format? If so, use the "
                                  "command-line option \"--qualitybase 33\" or "
                                  "\"--qualitybase solexa\"\n\n"

                                  "See README for more information.");
            } else {
                AR_DEBUG_FAIL("TODO");
            }
        } else {
            AR_DEBUG_FAIL("Unexpected offset in fastq_encoding::decode");
        }
    } else if (raw > max_score) {
        if (raw > '~') {
            throw fastq_error("ASCII value of quality score is greater than "
                              "126 (ASCII > '~'); input is corrupt or not in "
                              "FASTQ format!");
        } else if (offset == 33) {
            std::stringstream ss;

            ss << "Phred+33 encoded quality score is greater than the "
               << "expected maximum of " << static_cast<int>(max_score) << " ("
               << static_cast<char>(offset + max_score) << "). Please "
               << "verify the format of these files.\n\n"

               << "If the quality scores are actually Phred+64 encoded, then "
               << "use the '--qualitybase 64' command-line option.\n\n"

               << "If the quality scores are Phred+33 encoded, but includes "
               << "scores in a greater range than expected, then use the "
               << "'--qualitymax' option. Note that this option effects both "
               << "reading and writing of FASTQ files.\n\n"

               << "See README for more information.";

            throw fastq_error(ss.str());
        } else if (offset == 64) {
            std::stringstream ss;

            ss << "Phred+64 encoded quality score is greater than the "
               << "expected maximum of " << static_cast<int>(max_score) << " ("
               << static_cast<char>(offset + max_score) << "). Please "
               << "verify the format of these files.\n\n"

               << "If the quality scores are Phred+64 encoded, but includes "
               << "scores in a greater range than expected, then use the "
               << "'--qualitymax' command-line option. Note that this option "
               << "effects both reading and writing of FASTQ files.\n\n"

               << "See README for more information.";

            throw fastq_error(ss.str());
        } else {
            AR_DEBUG_FAIL("Unexpected offset in fastq_encoding::decode");
        }
    } else {
        AR_DEBUG_FAIL("invalid_phred called on valid PHRED score");
    }
}


[[noreturn]] void invalid_solexa(const char offset, const char max_score, const char raw)
{
    if (raw < ';') {
        if (raw < '!') {
            throw fastq_error("ASCII value of quality score is less than 33 "
                              "(ASCII < '!'); input is corrupt or not in "
                              "FASTQ format!");
        } else {
            throw fastq_error("Solexa score is less than -5 (ASCII = ';'); "
                              "Is this actually Phred+33 data? If so, use "
                              "the '--qualitybase 33' command-line option.\n\n"
                              "See the README for more information.");
        }
    } else if (raw > '~') {
        throw fastq_error("ASCII value of quality score is greater than "
                        "126 (ASCII > '~'); input is corrupt or not in "
                            "FASTQ format!");
    } else {
        std::stringstream ss;

        ss << "Solaxa encoded quality score is greater than the "
            << "expected maximum of " << static_cast<int>(max_score) << " ("
            << static_cast<char>(offset + max_score) << "). Please "
            << "verify the format of these files.\n\n"

            << "If the quality scores are Solexa encoded, but includes "
            << "scores in a greater range than expected, then use the "
            << "'--qualitymax' command-line option. Note that this option "
            << "effects both reading and writing of FASTQ files.\n\n"

            << "See README for more information.";

        throw fastq_error(ss.str());
    }
}


///////////////////////////////////////////////////////////////////////////////

namespace {

[[noreturn]] void
throw_invalid_base(char c)
{
  std::ostringstream stream;

  switch (c) {
    case 'B': // C / G / T
    case 'D': // A / G / T
    case 'H': // A / C / T
    case 'K': // G / T
    case 'M': // A / C
    case 'R': // A / G
    case 'S': // C / G
    case 'V': // A / C / G
    case 'W': // A / T
    case 'Y': // C / T
      stream << "found degenerate base '" << c << "' in FASTQ sequence, but "
             << "only bases A, C, G, T and N are supported. Use the option "
                "--mask-degenerate-bases to convert degenerate bases to N";
      break;

    case 'U': // Uracils
      stream << "found uracil (U) in FASTQ sequence, but only bases A, C, G, "
                "T and N are supported. Use the option --convert-uracils to "
                "convert uracils (U) to thymine (T)";
      break;

    default:
      stream << "invalid character " << std::string(1, c)
             << " found in FASTQ sequence";
      break;
  }

  throw fastq_error(stream.str());
}

void
process_nucleotides_strict(std::string& nucleotides)
{
  for (char& nuc : nucleotides) {
    // Fast ASCII letter uppercase
    const auto upper_case = nuc & 0xDF;
    switch (upper_case) {
      case 'A':
      case 'C':
      case 'G':
      case 'T':
      case 'N':
        nuc = upper_case;
        break;

      default:
        if (nuc == '.') {
            nuc = 'N';
        } else {
            throw_invalid_base(nuc);
        }
    }
  }
}

void
process_nucleotides_lenient(std::string& nucleotides,
                            const bool convert_uracil,
                            const bool mask_degenerate)
{
  for (char& nuc : nucleotides) {
    // Fast ASCII letter uppercase
    const auto upper_case = nuc & 0xDF;
    switch (upper_case) {
      case 'A':
      case 'C':
      case 'G':
      case 'T':
      case 'N':
        nuc = upper_case;
        break;

      // Uracils
      case 'U':
        if (convert_uracil) {
          nuc = 'T';
          break;
        } else {
          throw_invalid_base(nuc);
        }

      // IUPAC encoded degenerate bases
      case 'B': // C / G / T
      case 'D': // A / G / T
      case 'H': // A / C / T
      case 'K': // G / T
      case 'M': // A / C
      case 'R': // A / G
      case 'S': // C / G
      case 'V': // A / C / G
      case 'W': // A / T
      case 'Y': // C / T
        if (mask_degenerate) {
          nuc = 'N';
          break;
        } else {
          throw_invalid_base(nuc);
        }

      default:
        if (nuc == '.') {
            nuc = 'N';
        } else {
            throw_invalid_base(nuc);
        }
    }
  }
}

}


fastq_encoding::fastq_encoding(char offset, char max_score)
  : m_offset(offset)
  , m_max_score(std::min<size_t>('~' - offset, max_score))
  , m_mask_degenerate(false)
  , m_convert_uracil(false)
{
    if (offset != 33 && offset != 64) {
        throw std::invalid_argument("Phred offset must be 33 or 64");
    } else if (max_score < 0) {
        throw std::invalid_argument("Max ASCII encoded Phred score less than 0");
    } else if (max_score > '~' - PHRED_OFFSET_33) {
        throw std::invalid_argument("ASCII value cutoff for quality scores "
                                    "lies after printable characters");
    }
}


fastq_encoding::~fastq_encoding()
{
}


void fastq_encoding::decode_nucleotides(std::string& sequence) const
{
  if (m_mask_degenerate || m_convert_uracil) {
    process_nucleotides_lenient(sequence, m_convert_uracil, m_mask_degenerate);
  } else {
    process_nucleotides_strict(sequence);
  }
}


void fastq_encoding::encode_qualities(const std::string& qualities,
                                      std::string& dst) const
{
    const char ascii_max = m_offset + m_max_score;
    const char offset = m_offset - '!';

    for (const auto& quality : qualities) {
        dst.push_back(std::min<int>(ascii_max, quality + offset));
    }
}


void fastq_encoding::decode_qualities(std::string& qualities) const
{
    const char max_score = m_offset + m_max_score;
    const char offset = PHRED_OFFSET_33 - m_offset;

    for (auto& quality : qualities) {
        if (quality < m_offset || quality > max_score) {
            invalid_phred(m_offset, m_max_score, quality);
        }

        quality += offset;
    }
}


const char* fastq_encoding::name() const
{
    if (m_offset == 33) {
        return "Phred+33";
    } else if (m_offset == 64) {
        return "Phred+64";
    } else {
        AR_DEBUG_FAIL("Unexpected offset in fastq_encoding::name");
    }
}


size_t fastq_encoding::max_score() const
{
    return m_max_score;
}


fastq_encoding_solexa::fastq_encoding_solexa(unsigned max_score)
  : fastq_encoding(PHRED_OFFSET_64, max_score)
{
}


void fastq_encoding_solexa::encode_qualities(const std::string& qualities,
                                             std::string& dst) const
{
    const char ascii_max = m_offset + m_max_score;

    for (const auto& quality : qualities) {
        dst.push_back(std::min<int>(ascii_max, g_phred_to_solexa.at(quality - '!') + '@'));
    }
}


void fastq_encoding_solexa::decode_qualities(std::string& qualities) const
{
    const char max_score = m_offset + m_max_score;
    for (auto& quality : qualities) {
        if (quality < ';' || quality > max_score) {
            invalid_solexa(m_offset, m_max_score, quality);
        }

        quality = g_solexa_to_phred.at(quality - ';') + '!';
    }
}


const char* fastq_encoding_solexa::name() const
{
    return "Solexa";
}

} // namespace ar
