/*
    libmaus2
    Copyright (C) 2019 German Tischler-Höhle

    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/>.
*/
#if ! defined(LIBMAUS2_PROTEIN_ALIGNGLOBAL_HPP)
#define LIBMAUS2_PROTEIN_ALIGNGLOBAL_HPP

#include <libmaus2/protein/Blosum62.hpp>
#include <libmaus2/autoarray/AutoArray.hpp>
#include <libmaus2/autoarray/AutoArray2d.hpp>
#include <libmaus2/util/NumberSerialisation.hpp>

/**
 * global protein alignment
 *
 * not bug free yet, do not use
 **/
namespace libmaus2
{
	namespace protein
	{
		template<typename _score_type>
		struct AlignGlobal
		{
			typedef _score_type score_type;

			int64_t const gap_open_score;
			int64_t const gap_extend_score;

			enum trace_step { trace_m = 0, trace_d = 1, trace_i = 2, trace_x = 3, trace_none = 4 };

			static char const * stepToString(trace_step const & step)
			{
				switch ( step )
				{
					case trace_m: return "M";
					case trace_d: return "D";
					case trace_i: return "I";
					case trace_x: return "X";
					case trace_none: return "-";
					default: return "?";
				}
			}

			struct TraceElement
			{
				int64_t score;
				trace_step step;
				std::size_t count;

				TraceElement()
				{
				}

				TraceElement(
					int64_t const rscore,
					trace_step const rstep,
					std::size_t const rcount
				) : score(rscore), step(rstep), count(rcount)
				{

				}

				std::string toString() const
				{
					std::ostringstream ostr;
					ostr << "TraceElement(score=" << score << ",step=" << stepToString(step) << ",count=" << count << ")";
					return ostr.str();
				}
			};

			libmaus2::autoarray::AutoArray2d<TraceElement> AT;
			libmaus2::autoarray::AutoArray<trace_step> AS;

			AlignGlobal(
				int64_t const r_gap_open_score = -11,
				int64_t const r_gap_extend_score = -1
			) : gap_open_score(r_gap_open_score), gap_extend_score(r_gap_extend_score)
			{

			}

			int64_t getGapOpenScore() const
			{
				return gap_open_score;
			}

			int64_t getGapExtendScore() const
			{
				return gap_extend_score;
			}

			typedef std::tuple<int64_t,std::size_t> result_type;

			static std::string lformatNumber(uint64_t const n, uint64_t const len)
			{
				std::ostringstream str;
				str << std::setw(len) << std::setfill(' ') << n;
				return str.str();
			}

			template<typename iterator_a, typename iterator_b>
			std::ostream & printTrace(
				std::ostream & out,
				uint64_t const ATOo,
				iterator_a a,
				iterator_b b,
				uint64_t const cols = std::numeric_limits<uint64_t>::max(),
				std::string const & label_a = std::string(),
				std::string const & label_b = std::string(),
				uint64_t const rpos_a = 0,
				uint64_t const rpos_b = 0
			)
			{
				std::ostringstream astr;
				std::ostringstream bstr;
				std::ostringstream cstr;

				uint64_t const label_a_size = label_a.size();
				uint64_t const label_b_size = label_b.size();
				uint64_t const max_label_size = std::max(label_a_size,label_b_size);
				uint64_t const max_label_padded = max_label_size ? (max_label_size+1) : 0;
				uint64_t const pad_a_size = max_label_padded - label_a_size;
				uint64_t const pad_b_size = max_label_padded - label_b_size;

				std::string const print_label_a = label_a + std::string(pad_a_size,' ');
				std::string const print_label_b = label_b + std::string(pad_b_size,' ');
				std::string const indent = std::string(max_label_padded,' ');

				for ( uint64_t i = 0; i < ATOo; ++i )
					switch ( AS[i] )
					{
						case trace_m: cstr << ((*a==*b) ? '=' : 'X'); astr << *(a++); bstr << *(b++); break;
						case trace_i: cstr << 'I'                   ; astr << ' '   ; bstr << *(b++); break;
						case trace_d: cstr << 'D'                   ; astr << *(a++); bstr << ' '   ; break;
						case trace_x: default:  break;
					}

				std::string const sa = astr.str();
				std::string const sb = bstr.str();
				std::string const sc = cstr.str();

				assert ( sa.size() == sb.size() );
				assert ( sb.size() == sc.size() );

				uint64_t r = sa.size();
				uint64_t pos_a = rpos_a;
				uint64_t pos_b = rpos_b;
				char const * ca = sa.c_str();
				char const * cb = sb.c_str();
				char const * cc = sc.c_str();

				uint64_t maxpos_a = rpos_a;
				uint64_t maxpos_b = rpos_b;

				while ( r )
				{
					uint64_t const s = std::min(cols,r);

					maxpos_a = pos_a;
					maxpos_b = pos_b;

					for ( uint64_t i = 0; i < s; ++i )
					{
						char const c = cc[i];

						switch ( c )
						{
							case '=':
							case 'X':
								pos_a++;
								pos_b++;
								break;
							case 'I':
								pos_b++;
								break;
							case 'D':
								pos_a++;
								break;
							default:
								break;
						}
					}

					cc += s;
					r -= s;
				}

				uint64_t const maxpos = std::max(maxpos_a,maxpos_b);
				uint64_t const numlen = libmaus2::util::NumberSerialisation::formatNumber(maxpos,0).size();

				ca = sa.c_str();
				cb = sb.c_str();
				cc = sc.c_str();
				r = sa.size();
				pos_a = rpos_a;
				pos_b = rpos_b;

				while ( r )
				{
					uint64_t const s = std::min(cols,r);

					std::string const spos_a = lformatNumber(pos_a,numlen);
					std::string const spos_b = lformatNumber(pos_b,numlen);
					std::string const spos_c(numlen,' ');

					for ( uint64_t i = 0; i < s; ++i )
					{
						char const c = cc[i];

						switch ( c )
						{
							case '=':
							case 'X':
								pos_a++;
								pos_b++;
								break;
							case 'I':
								pos_b++;
								break;
							case 'D':
								pos_a++;
								break;
							default:
								break;
						}
					}

					out << print_label_a << " " << spos_a << " "; out.write(ca,s); out.put('\n'); ca += s;
					out << print_label_b << " " << spos_b << " ";; out.write(cb,s); out.put('\n'); cb += s;
					out << indent        << " " << spos_c << " "; out.write(cc,s); out.put('\n'); cc += s;

					r -= s;
				}

				return out;
			}


			uint64_t enumerateMatches(
				uint64_t const ATOo,
				uint64_t const pos_a,
				uint64_t const pos_b,
				libmaus2::autoarray::AutoArray< std::pair<uint64_t,uint64_t> > & O
			) const
			{
				uint64_t p_a = pos_a;
				uint64_t p_b = pos_b;
				uint64_t o = 0;

				for ( uint64_t i = 0; i < ATOo; ++i )
					switch ( AS[i] )
					{
						case trace_m: O.push(o,std::make_pair(p_a,p_b)); ++p_a; ++p_b; break;
						case trace_i:        ++p_b; break;
						case trace_d: ++p_a;        break;
						case trace_x: default:      break;
					}

				return o;
			}

			std::pair<std::size_t,std::size_t> getStringLengthUsed(result_type const & R)
			{
				std::size_t const o = std::get<1>(R);

				std::size_t i = 0;
				std::size_t j = 0;

				for ( std::size_t z = 0; z < o; ++z )
				{
					switch ( AS[z] )
					{
						case trace_m:
						case trace_x:
							++i;
							++j;
							break;
						case trace_i:
							++j;
							break;
						case trace_d:
							++i;
							break;
						default:
							break;
					}
				}

				return std::make_pair(i,j);
			}

			template<typename iterator_a, typename iterator_b>
			void print(
				iterator_a const it_ab,
				iterator_b const it_bb,
				result_type const & R,
				std::ostream & out
			)
			{
				std::ostringstream astr;
				std::ostringstream bstr;
				std::ostringstream ostr;

				std::size_t const o = std::get<1>(R);

				std::size_t i = 0;
				std::size_t j = 0;

				for ( std::size_t z = 0; z < o; ++z )
				{
					switch ( AS[z] )
					{
						case trace_m:
							astr << it_ab[i++];
							bstr << it_bb[j++];
							ostr << "+";
							break;
						case trace_x:
							astr << it_ab[i++];
							bstr << it_bb[j++];
							ostr << "-";
							break;
						case trace_i:
							astr << ' ';
							bstr << it_bb[j++];
							ostr << "I";
							break;
						case trace_d:
							astr << it_ab[i++];
							bstr << ' ';
							ostr << "D";
							break;
						default:
							break;
					}
				}

				out << astr.str() << "\n";
				out << bstr.str() << "\n";
				out << ostr.str() << "\n";
			}


			template<typename iterator_a, typename iterator_b>
			result_type align(
				iterator_a const it_ab,
				iterator_a const it_ae,
				iterator_b const it_bb,
				iterator_b const it_be
			)
			{
				std::size_t const n = it_ae - it_ab;
				std::size_t const m = it_be - it_bb;

				AT = libmaus2::autoarray::AutoArray2d<TraceElement>(n+1,m+1,false);

				AT(0,0) = TraceElement(0,trace_none,0);

				for ( std::size_t i = 0; i < n; ++i )
					AT(i+1,0) = TraceElement(gap_open_score + i * gap_extend_score,trace_d,i+1);
				for ( std::size_t i = 0; i < m; ++i )
					AT(0,i+1) = TraceElement(gap_open_score + i * gap_extend_score,trace_i,i+1);

				for ( std::size_t ii = 0; ii < n; ++ii )
				{
					char const ca = it_ab[ii];
					std::size_t i = ii+1;

					for ( std::size_t jj = 0; jj < m; ++jj )
					{
						char const cb = it_bb[jj];
						std::size_t j = jj+1;

						TraceElement & T = AT(i,j);

						T.score = std::numeric_limits<int64_t>::min();

						for ( std::size_t y = 0; y < i; ++y )
						{
							TraceElement const & TP = AT(y,j);
							int64_t const dist = i - y;
							assert ( dist );
							int64_t const extend = dist - 1;
							int64_t const lscore = TP.score + gap_extend_score + extend*gap_extend_score;

							if ( lscore > T.score )
								T = TraceElement(lscore,trace_d,dist);
						}

						for ( std::size_t z = 0; z < j; ++z )
						{
							TraceElement const & TP = AT(j,z);
							int64_t const dist = j - z;
							assert ( dist );
							int64_t const extend = dist - 1;
							int64_t const lscore = TP.score + gap_extend_score + extend*gap_extend_score;

							if ( lscore > T.score )
								T = TraceElement(lscore,trace_i,dist);
						}

						{
							TraceElement const & TP = AT(ii,jj);
							int64_t const lscore = TP.score + score_type::getScore(ca,cb);

							if ( lscore > T.score )
								T = TraceElement(lscore,(ca == cb) ? trace_m : trace_x,1);
						}
					}
				}

				std::size_t i = n;
				std::size_t j = m;
				uint64_t o = 0;

				while ( i || j )
				{
					TraceElement const & T = AT(i,j);

					// std::cerr << "i=" << i << " j=" << j << " " << T.toString() << std::endl;

					for ( std::size_t z = 0; z < T.count; ++z )
						AS.push(o,T.step);

					switch ( T.step )
					{
						case trace_m:
						case trace_x:
							assert ( i >= T.count );
							assert ( j >= T.count );
							i -= T.count;
							j -= T.count;
							break;
						case trace_d:
							assert ( i >= T.count );
							i -= T.count;
							break;
						case trace_i:
							assert ( j >= T.count );
							j -= T.count;
							break;
						default:
							break;
					}
				}

				std::reverse(AS.begin(),AS.begin()+o);

				return result_type(AT(n,m).score,o);
			}

			template<typename iterator_a, typename iterator_b>
			result_type alignPrefix(
				iterator_a const it_ab,
				iterator_a const it_ae,
				iterator_b const it_bb,
				iterator_b const it_be
			)
			{
				std::size_t n;
				std::size_t m;

				{
					int64_t const za = it_ae-it_ab;
					int64_t const lb = it_be-it_bb;

					// lb*max_score + gap_open_score + i*gap_extend_score < 0
					// lb*max_score + gap_open_score < i * -gap_extend_score
					// i > (lb*max_score + gap_open_score) / - gap_extend_score
					int64_t const maxscore = score_type::getMaxScore();
					int64_t const basescore = lb * maxscore + gap_open_score;
					int64_t const m_gap_extend_score = - gap_extend_score;
					int64_t const cmaxdel = (basescore + m_gap_extend_score - 1) / m_gap_extend_score + 1;
					int64_t const maxdel = std::min(cmaxdel,static_cast<int64_t>(10));
					int64_t const maxusea = (maxdel > 0) ? maxdel + lb : za;
					int64_t const la = std::min(za,maxusea);

					n = la;
					m = lb;
				}

				#if 0
				std::cerr << "a=" << std::string(it_ab,it_ab+n) << std::endl;
				std::cerr << "b=" << std::string(it_bb,it_bb+m) << std::endl;
				#endif

				AT = libmaus2::autoarray::AutoArray2d<TraceElement>(n+1,m+1,false);

				AT(0,0) = TraceElement(0,trace_none,0);

				for ( std::size_t i = 0; i < n; ++i )
					AT(i+1,0) = TraceElement(gap_open_score + i * gap_extend_score,trace_d,i+1);
				for ( std::size_t i = 0; i < m; ++i )
					AT(0,i+1) = TraceElement(gap_open_score + i * gap_extend_score,trace_i,i+1);

				int64_t maxscore = std::numeric_limits<int64_t>::min();
				int64_t maxi = std::numeric_limits<int64_t>::min();
				int64_t maxj = std::numeric_limits<int64_t>::min();

				for ( std::size_t ii = 0; ii < n; ++ii )
				{
					char const ca = it_ab[ii];
					std::size_t i = ii+1;

					for ( std::size_t jj = 0; jj < m; ++jj )
					{
						char const cb = it_bb[jj];
						std::size_t j = jj+1;

						TraceElement & T = AT(i,j);

						T.score = std::numeric_limits<int64_t>::min();

						for ( std::size_t y = 0; y < i; ++y )
						{
							TraceElement const & TP = AT(y,j);
							int64_t const dist = i - y;
							assert ( dist );
							int64_t const extend = dist - 1;
							int64_t const lscore = TP.score + gap_extend_score + extend*gap_extend_score;

							if ( lscore > T.score )
								T = TraceElement(lscore,trace_d,dist);
						}

						for ( std::size_t z = 0; z < j; ++z )
						{
							TraceElement const & TP = AT(j,z);
							int64_t const dist = j - z;
							assert ( dist );
							int64_t const extend = dist - 1;
							int64_t const lscore = TP.score + gap_extend_score + extend*gap_extend_score;

							if ( lscore > T.score )
								T = TraceElement(lscore,trace_i,dist);
						}

						{
							TraceElement const & TP = AT(ii,jj);
							int64_t const lscore = TP.score + score_type::getScore(ca,cb);

							if ( lscore > T.score )
								T = TraceElement(lscore,(ca == cb) ? trace_m : trace_x,1);
						}

						if ( j == m )
						{
							if ( T.score > maxscore )
							{
								maxscore = T.score;
								maxi = i;
								maxj = j;

								#if 0
								std::cerr << "maxscore=" << maxscore << " i=" << i << std::endl;
								#endif
							}
						}
					}
				}

				if ( maxi < 0 )
					return result_type(maxscore,0);

				std::size_t i = static_cast<std::size_t>(maxi);
				std::size_t j = static_cast<std::size_t>(maxj);
				uint64_t o = 0;

				while ( i || j )
				{
					TraceElement const & T = AT(i,j);

					// std::cerr << "i=" << i << " j=" << j << " " << T.toString() << std::endl;

					for ( std::size_t z = 0; z < T.count; ++z )
						AS.push(o,T.step);

					switch ( T.step )
					{
						case trace_m:
						case trace_x:
							assert ( i >= T.count );
							assert ( j >= T.count );
							i -= T.count;
							j -= T.count;
							break;
						case trace_d:
							assert ( i >= T.count );
							i -= T.count;
							break;
						case trace_i:
							assert ( j >= T.count );
							j -= T.count;
							break;
						default:
							break;
					}
				}

				std::reverse(AS.begin(),AS.begin()+o);

				return result_type(AT(maxi,maxj).score,o);
			}

			struct AlignResult
			{
				uint64_t startpos_a;
				uint64_t endpos_a;
				uint64_t startpos_b;
				uint64_t endpos_b;
				uint64_t al_O;
				int64_t score;

				AlignResult() : startpos_a(0), endpos_a(0), startpos_b(0), endpos_b(0), al_O(0), score(std::numeric_limits<int64_t>::min()) {}
				AlignResult(
					uint64_t rstartpos_a,
					uint64_t rendpos_a,
					uint64_t rstartpos_b,
					uint64_t rendpos_b,
					uint64_t ral_O,
					int64_t rscore
				) : startpos_a(rstartpos_a), endpos_a(rendpos_a), startpos_b(rstartpos_b), endpos_b(rendpos_b), al_O(ral_O), score(rscore)
				{}
			};

			template<
				typename it_a,
				typename it_b
			>
			AlignResult align(
				it_a const aa,
				it_a const ae,
				uint64_t const seedposa,
				it_b const bb,
				it_b const be,
				uint64_t const seedposb
			)
			{
				result_type const RF = alignPrefix(aa+seedposa,ae,bb+seedposb,be);

				std::pair<uint64_t,uint64_t> const SL_0 = getStringLengthUsed(RF);

				// assert ( be - (bb+seedposb) == static_cast<int64_t>(SL_0.second) );

				uint64_t const endposa = seedposa + SL_0.first;
				uint64_t const endposb = seedposb + SL_0.second;

				#if 0
				std::cerr << "endposa=" << endposa << std::endl;
				std::cerr << "endposb=" << endposb << std::endl;
				#endif

				std::reverse_iterator<it_a> raa(aa + endposa);
				std::reverse_iterator<it_a> rae(aa);
				std::reverse_iterator<it_b> rba(bb + endposb);
				std::reverse_iterator<it_b> rbe(bb);

				result_type const RR = alignPrefix(raa,rae,rba,rbe);

				std::pair<uint64_t,uint64_t> const SL_1 = getStringLengthUsed(RR);

				// assert ( static_cast<int64_t>(SL_1.second) == be-bb );

				uint64_t const startposa = endposa - SL_1.first;
				uint64_t const startposb = endposb - SL_1.second;

				#if 0
				std::cerr << "startposa=" << startposa << std::endl;
				std::cerr << "startposb=" << startposb << std::endl;
				#endif

				result_type const R = align(aa + startposa,aa + endposa,bb + startposb,bb + endposb);

				int64_t const score = std::get<0>(R);

				AlignResult const AR(startposa,endposa,startposb,endposb,std::get<1>(R),score);

				return AR;
			}

		};

		typedef AlignGlobal< ::libmaus2::protein::Blosum62> AlignGlobalBlosum62;
	}
}
#endif
