/*---------------------------------------------------------------------\
|                          ____ _   __ __ ___                          |
|                         |__  / \ / / . \ . \                         |
|                           / / \ V /|  _/  _/                         |
|                          / /__ | | | | | |                           |
|                         /_____||_| |_| |_|                           |
|                                                                      |
----------------------------------------------------------------------*/

#include <iostream>
#include <cstring>
#include <cstdlib>

#include <zypp-core/base/LogTools.h>
#include <zypp-core/base/String.h>
#include <zypp-core/base/DtorReset>
#include <zypp-core/base/Gettext.h>

#include <zypp-tui/Application>
#include <zypp-tui/utils/colors.h>
#include <zypp-tui/utils/console.h>
#include <zypp-tui/utils/text.h>

#include "Table.h"

// libzypp logger settings
#undef  ZYPP_BASE_LOGGER_LOGGROUP
#define ZYPP_BASE_LOGGER_LOGGROUP "zypper"

namespace ztui {

const char * asYesNo( bool val_r ) { return val_r ? _("Yes") : _("No"); }

TableLineStyle Table::defaultStyle = Ascii;

static const char * lines[][3] = {
  { "|", "-", "+"},		///< Ascii
  // utf 8
  { "\xE2\x94\x82", "\xE2\x94\x80", "\xE2\x94\xBC" },	///< light
  { "\xE2\x94\x83", "\xE2\x94\x81", "\xE2\x95\x8B" },	///< heavy
  { "\xE2\x95\x91", "\xE2\x95\x90", "\xE2\x95\xAC" },	///< double
  { "\xE2\x94\x86", "\xE2\x94\x84", "\xE2\x94\xBC" },	///< light 3
  { "\xE2\x94\x87", "\xE2\x94\x85", "\xE2\x94\x8B" },	///< heavy 3
  { "\xE2\x94\x82", "\xE2\x94\x81", "\xE2\x94\xBF" },	///< v light, h heavy
  { "\xE2\x94\x82", "\xE2\x95\x90", "\xE2\x95\xAA" },	///< v light, h double
  { "\xE2\x94\x83", "\xE2\x94\x80", "\xE2\x95\x82" },	///< v heavy, h light
  { "\xE2\x95\x91", "\xE2\x94\x80", "\xE2\x95\xAB" },	///< v double, h light
  { ":", "-", "+" },					///< colon separated values
};


namespace {
  /// Compare wchar_t case sensitive.
  inline int wccmp( const wchar_t & l, const wchar_t & r )
  { return l == r ? 0 : l < r ? -1 : 1; }

  /// Compare wchar_t case insensitive.
  inline int wccasecmp( const wchar_t & l, const wchar_t & r )
  { return ::wcsncasecmp( &l, &r, 1 ); }

  /// Whether wchar_t is a Zero digit.
  inline bool isZero( const wchar_t & ch )
  { return ch == L'0'; }

  /// Whether wchar_t is a digit.
  inline bool isDigit( const wchar_t & ch )
  { return ::iswdigit( ch ); }

  /// Whether both wchar_t are digits.
  inline bool bothDigits( const wchar_t & l, const wchar_t & r )
  { return isDigit( l ) && isDigit( r ); }

  /// Whether both wchar_t are no digits.
  inline bool bothNotDigits( const wchar_t & l, const wchar_t & r )
  { return not ( isDigit( l ) || isDigit( r ) ); }

  /// Whether both are at the end of the string.
  inline bool bothAtEnd( const mbs::MbsIteratorNoSGR & lit, const mbs::MbsIteratorNoSGR & rit )
  { return lit.atEnd() && rit.atEnd(); }

  /// Whether there are one or more trailing Zeros.
  inline bool skipTrailingZeros( mbs::MbsIteratorNoSGR & it )
  {
    if ( isZero( *it ) ) {
      do { ++it; } while ( isZero( *it ) );
      return it.atEnd();
    }
    return false;
  }

  /// compare like numbers: longer digit sequence wins, otherwise first difference
  inline int wcnumcmpValue( mbs::MbsIteratorNoSGR & lit, mbs::MbsIteratorNoSGR & rit )
  {
    // PRE: no leading Zeros
    // POST: if 0(equal) is returned, all digis were skipped
    int diff = 0;
    for ( ;; ++lit, ++rit ) {
      if ( isDigit( *lit ) ) {
        if ( isDigit( *rit ) ) {
          if ( not diff && *lit != *rit )
            diff = *lit < *rit ? -1 : 1;
        }
        else
          return 1;     // DIG !DIG
      }
      else {
        if ( isDigit( *rit ) )
          return -1;    // !DIG DIG
        else
          return diff;  // !DIG !DIG
      }
    }
  }
} // namespace

int TableRow::Less::defaultStrComp( bool ci_r, const std::string & lhs, const std::string & rhs )
{
  auto wcharcmp = &wccmp; // always start with case sensitive compare
  int nbias = 0;          // remember the 1st difference (in case num compare equal)
  int cbias = 0;          // remember the 1st difference (in case ci compare equal)
  int cmp = 0;
  mbs::MbsIteratorNoSGR lit { lhs };
  mbs::MbsIteratorNoSGR rit { rhs };
  while ( true ) {

    // Endgame: tricky: trailing Zeros are ignored, but count as nbias if there is none.
    if ( lit.atEnd() ) {
      if ( skipTrailingZeros( rit ) && not nbias ) return -1;
      return rit.atEnd() ? (nbias ? nbias : cbias) : -1;
    }
    if ( rit.atEnd() ) {
      if ( skipTrailingZeros( lit ) && not nbias ) return 1;
      return lit.atEnd() ? (nbias ? nbias : cbias) : 1;
    }

    // num <> num?
    if ( bothDigits( *lit, *rit ) ) {
      if ( isZero( *lit ) || isZero( *rit ) ) {
        int lead = 0; // the more leasing zeros a number has, the less: 001 01 1
        while ( isZero( *lit ) ) { ++lit; --lead; }
        while ( isZero( *rit ) ) { ++rit; ++lead; }
        if ( not nbias && lead )
          nbias = bothAtEnd( lit, rit ) ? -lead : lead;  // the less trailing zeros, the less: a a0 a00
      }
      if ( (cmp = wcnumcmpValue( lit, rit )) )
        return cmp;
      continue; // already skipped all digits
    }
    else {
      if ( (cmp = wcharcmp( *lit, *rit )) ) {
        if ( not cbias ) cbias = cmp; // remember the 1st difference (by wccmp)
        if ( ci_r ) {
          if ( (cmp = wccasecmp( *lit, *rit )) )
            return cmp;
          wcharcmp = &wccasecmp;
          ci_r = false;
        }
        else
          return cmp;
      }
    }
    ++lit; ++rit;
  }
}

TableRow & TableRow::add( std::string s )
{
  if ( _translateColumns )
    _translatedColumns.push_back( _(s.c_str()) );
  _columns.push_back( std::move(s) );
  return *this;
}

TableRow & TableRow::addDetail( std::string s )
{
  _details.push_back( std::move(s) );
  return *this;
}

// 1st implementation: no width calculation, just tabs
std::ostream & TableRow::dumbDumpTo( std::ostream & stream ) const
{
  bool seen_first = false;
  for ( container::const_iterator i = _columns.begin(); i != _columns.end(); ++i )
  {
    if ( seen_first )
      stream << '\t';
    seen_first = true;

    stream << *i;
  }
  return stream << std::endl;
}

std::ostream & TableRow::dumpDetails( std::ostream & stream, const Table & parent ) const
{
  mbs::MbsWriteWrapped mww( stream, 4, parent._screen_width );
  for ( const std::string & text : _details )
  {
    mww.writePar( text );
  }
  mww.gotoParBegin();
  return stream;
}

std::ostream & TableRow::dumpTo( std::ostream & stream, const Table & parent ) const
{
  const char * vline = parent._style == none ? "" : lines[parent._style][0];

  unsigned ssize = 0; // string size in columns
  bool seen_first = false;

  stream.setf( std::ios::left, std::ios::adjustfield );
  stream << std::string( parent._margin, ' ' );
  // current position at currently printed line
  int curpos = parent._margin;
  // On a table with 2 edition columns highlight the editions
  // except for the common prefix.
  std::string::size_type editionSep( std::string::npos );

  container::const_iterator i = _columns.begin (), e = _columns.end ();
  const unsigned lastCol = _columns.size() - 1;
  for ( unsigned c = 0; i != e ; ++i, ++c )
  {
    const std::string & s( *i );

    if ( seen_first )
    {
      bool do_wrap = parent._do_wrap				// user requested wrapping
                  && parent._width > parent._screen_width	// table is wider than screen
                  && ( curpos + (int)parent._max_width[c] + (parent._style == none ? 2 : 3) > parent._screen_width	// the next table column would exceed the screen size
                    || parent._force_break_after == (int)(c - 1) );	// or the user wishes to first break after the previous column

      if ( do_wrap )
      {
        // start printing the next table columns to new line,
        // indent by 2 console columns
        stream << std::endl << std::string( parent._margin + 2, ' ' );
        curpos = parent._margin + 2; // indent == 2
      }
      else
        // vertical line, padded with spaces
        stream << ' ' << vline << ' ';
      stream.width( 0 );
    }
    else
      seen_first = true;

    // stream.width (widths[c]); // that does not work with multibyte chars
    ssize = mbs_width( s );
    if ( ssize > parent._max_width[c] )
    {
      unsigned cutby = parent._max_width[c] - 2;
      std::string cutstr = mbs_substr_by_width( s, 0, cutby );
      stream << ( _ctxt << cutstr ) << std::string(cutby - mbs_width( cutstr ), ' ') << "->";
    }
    else
    {
      if ( !parent._inHeader && parent.header().hasStyle( c, table::CStyle::Edition ) && Application::instance().config().do_colors )
      {
        const std::set<unsigned> & editionColumns { parent.header().editionColumns() };
        // Edition column
        if ( editionColumns.size() == 2 )
        {
          // 2 Edition columns - highlight difference
          if ( editionSep == std::string::npos )
          {
            editionSep = zypp::str::commonPrefix( _columns[*editionColumns.begin()],
                                            _columns[*(++editionColumns.begin())] );
          }

          if ( editionSep == 0 )
          {
            stream << ( ColorContext::CHANGE << s );
          }
          else if ( editionSep == s.size() )
          {
            stream << ( _ctxt << s );
          }
          else
          {
            stream << ( _ctxt << s.substr( 0, editionSep ) ) << ( ColorContext::CHANGE << s.substr( editionSep ) );
          }
        }
        else
        {
          // highlight edition-release separator
          editionSep = s.find( '-' );
          if ( editionSep != std::string::npos )
          {
            stream << ( _ctxt << s.substr( 0, editionSep ) << ( ColorContext::HIGHLIGHT << "-" ) << s.substr( editionSep+1 ) );
          }
          else	// no release part
          {
            stream << ( _ctxt << s );
          }
        }
      }
      else	// no special style
      {
        stream << ( _ctxt << s );
      }
      stream.width( c == lastCol ? 0 : parent._max_width[c] - ssize );
    }
    stream << "";
    curpos += parent._max_width[c] + (parent._style == none ? 2 : 3);
  }
  stream << std::endl;

  if ( !_details.empty() )
  {
    dumpDetails( stream, parent );
  }
  return stream;
}

// ----------------------( Table )---------------------------------------------

Table::Table()
  : _has_header( false )
  , _max_col( 0 )
  , _max_width( 1, 0 )
  , _width( 0 )
  , _style( defaultStyle )
  , _screen_width( get_screen_width() )
  , _margin( 0 )
  , _force_break_after( -1 )
  , _do_wrap( false )
  , _inHeader( false )
{}

Table & Table::add( TableRow tr )
{
  _rows.push_back( std::move(tr) );
  return *this;
}

Table & Table::setHeader( TableHeader tr )
{
  _header = std::move(tr);
  _has_header = !_header.empty();
  return *this;
}

void Table::allowAbbrev( unsigned column)
{
  if ( column >= _abbrev_col.size() )
  {
    _abbrev_col.reserve( column + 1 );
    _abbrev_col.insert( _abbrev_col.end(), column - _abbrev_col.size() + 1, false );
  }
  _abbrev_col[column] = true;
}

void Table::updateColWidths( const TableRow & tr ) const
{
  // how much columns spearators add to the width of the table
  int sepwidth = _style == none ? 2 : 3;
  // initialize the width to -sepwidth (the first column does not have a line
  // on the left)
  _width = -sepwidth;

  // ensure that _max_width[col] exists
  const auto &columns = tr.columns();
  if ( _max_width.size() < columns.size() )
  {
    _max_width.resize( columns.size(), 0 );
    _max_col = _max_width.size()-1;
  }

  unsigned c = 0;
  for ( const auto & col : columns )
  {
    unsigned &max = _max_width[c++];
    unsigned cur = mbs_width( col );

    if ( max < cur )
      max = cur;

    _width += max + sepwidth;
  }
  _width += _margin * 2;
}

void Table::dumpRule( std::ostream &stream ) const
{
  const char * hline = _style != none ? lines[_style][1] : " ";
  const char * cross = _style != none ? lines[_style][2] : " ";

  bool seen_first = false;

  stream.width( 0 );
  stream << std::string(_margin, ' ' );
  for ( unsigned c = 0; c <= _max_col; ++c )
  {
    if ( seen_first )
      stream << hline << cross << hline;
    seen_first = true;
    // FIXME: could use fill character if hline were a (wide) character
    for ( unsigned i = 0; i < _max_width[c]; ++i )
      stream << hline;
  }
  stream << std::endl;
}

std::ostream & Table::dumpTo( std::ostream & stream ) const
{
  // compute column sizes
  if ( _has_header )
    updateColWidths( _header );
  for ( const auto & row : _rows )
    updateColWidths( row );

  // reset column widths for columns that can be abbreviated
  //! \todo allow abbrev of multiple columns?
  unsigned c = 0;
  for ( std::vector<bool>::const_iterator it = _abbrev_col.begin(); it != _abbrev_col.end() && c <= _max_col; ++it, ++c )
  {
    if ( *it && _width > _screen_width &&
         // don't resize the column to less than 3, or if the resulting table
         // would still exceed the screen width (bnc #534795)
         _max_width[c] > 3 &&
         _width - _screen_width < ((int) _max_width[c]) - 3 )
    {
      _max_width[c] -= _width - _screen_width;
      break;
    }
  }

  if ( _has_header )
  {
    zypp::DtorReset inHeader( _inHeader, false );
    _inHeader = true;
    _header.dumpTo( stream, *this );
    dumpRule (stream);
  }

  for ( const auto & row : _rows )
    row.dumpTo( stream, *this );

  return stream;
}

void Table::wrap( int force_break_after )
{
  if ( force_break_after >= 0 )
    _force_break_after = force_break_after;
  _do_wrap = true;
}

void Table::lineStyle( TableLineStyle st )
{
  if ( st < TLS_End )
    _style = st;
}

void Table::margin( unsigned margin )
{
  if ( margin < (unsigned)(_screen_width/2) )
    _margin = margin;
  else
    ERR << "margin of " << margin << " is greater than half of the screen" << std::endl;
}

// Local Variables:
// c-basic-offset: 2
// End:
}
