/* textstorage.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2005-2023 Ralf Hoffmann.
 * You can contact me at: ralf@boomerangsworld.de
 *   or http://www.boomerangsworld.de/worker
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "textstorage.h"
#include "utf8.hh"
#include "util.h"

TextStorageString::TextStorageString( const char *buf,
                                      int len,
                                      const RefCount<AWidth> &lencalc,
                                      int line_limit,
                                      enum text_storage_converion convert ) : m_convert( convert ),
                                                                              m_lencalc( lencalc ),
                                                                              m_current_line_limit( -1 )
{
  std::string s1( buf, len );

  m_raw_buffer = s1;

  if ( m_convert != NO_CHARACTER_CONVERSION ) {
      convertRawData();
  } else {
      buffer = m_raw_buffer;
  }
  
  initBuffer( line_limit );
}

TextStorageString::TextStorageString( const std::string &content,
                                      const RefCount<AWidth> &lencalc,
                                      int line_limit,
                                      enum text_storage_converion convert ) : m_convert( convert ),
                                                                              m_lencalc( lencalc ),
                                                                              m_current_line_limit( -1 )
{
    m_raw_buffer = content;

    if ( m_convert != NO_CHARACTER_CONVERSION ) {
        convertRawData();
    } else {
        buffer = m_raw_buffer;
    }
  
    initBuffer( line_limit );
}

int TextStorageString::getNrOfLines() const
{
  return line_offsets.size();
}

int TextStorageString::getLine( int line_nr, unsigned int offset, int len, std::string &return_line ) const
{
  size_t start_offset, line_len;  // remember offset from line_offsets are not necessarly inside buffer

  if ( ( line_nr < 0 ) || ( line_nr >= getNrOfLines() ) ) return 1;

  start_offset = line_offsets[ line_nr ];
  line_len = getLineCharacters( line_nr );

  return_line = "";

  if ( offset < line_len ) {
    // offset given still inside line

    if ( UTF8::isValidCharacter( buffer.c_str() + start_offset + offset ) == false ) {
      int n = (int)offset;
      UTF8::movePosToPrevChar( buffer.c_str() + start_offset, n );
      offset = (unsigned int )n;
    }

    start_offset += offset;
    line_len -= offset;
    
    if ( len >= 0 ) {
      // line limiter given
      if ( (unsigned int )len < line_len ) line_len = (unsigned int)len;
    }
    if ( ( start_offset < buffer.length() ) && ( line_len > 0 ) )
      return_line.append( buffer, start_offset, line_len );
  }
  return 0;
}

int TextStorageString::getLine( int line_nr, unsigned int offset, std::string &return_line ) const
{
  return getLine( line_nr, offset, -1, return_line );
}

void TextStorageString::initBuffer( int line_limit_width )
{
    size_t i;
    int current_line = 0, current_subline = 0;

    line_offsets.clear();
    line_offsets.push_back( 0 );
    _real_lines.clear();
    _real_lines.push_back( std::pair<int,int>( current_line, current_subline ) );
    m_line_width_cached.clear();

    m_unwrapped_line_offsets.clear();
    m_unwrapped_line_offsets.push_back( 0 );

    if ( line_limit_width < 1 ) {
        // the easiest stuff: no line limit
        for ( i = 0; i < buffer.length(); i++ ) {
            if ( buffer[i] == '\n' ) {
                current_line++;

                line_offsets.push_back( i + 1 );  // if newline is last character
                // this line offset will be outside
                // the buffer
                m_unwrapped_line_offsets.push_back( i + 1 );
                _real_lines.push_back( std::pair<int,int>( current_line, current_subline ) );
            }
        }
    } else {
        unsigned int cur_line_width, token_width, token_characters;
        bool newline;
    
        cur_line_width = 0;
    
        for ( i = 0; i < buffer.length();) {
            newline = false;
            if ( buffer[i] == '\n' ) {
                newline = true;
                i++;

                current_line++;
                current_subline = 0;
            } else {
                // assume each character takes at least one pixel and use that as an upper limit
                token_characters = getTokenLen( i, line_limit_width );
                if ( ( i + token_characters ) > buffer.length() ) {
                    token_characters = buffer.length() - i;
                }
        
                if ( token_characters < 1 )
                    token_characters = UTF8::getLenOfCharacter( buffer.c_str() + i );
                if ( token_characters < 1 )
                    token_characters = 1;
        
                token_width = m_lencalc->getWidth( buffer.c_str() + i, token_characters );

                if ( ( cur_line_width + token_width ) <= (unsigned int)line_limit_width ) {
                    cur_line_width += token_width;
                    i += token_characters;
                } else if ( cur_line_width == 0 ) {
                    // no character in current line and no space for whole token
                    // so split it
                    token_characters = m_lencalc->getStrlen4WidthMaxlen( buffer.c_str() + i, token_characters,
                                                                         line_limit_width, NULL );

                    if ( token_characters < 1 ) {
                        // no room for at least one character at line start so we need to take
                        // this first one even this will break the line limit
                        token_characters = UTF8::getLenOfCharacter( buffer.c_str() + i );
                        if ( token_characters < 1 )
                            token_characters = 1;
                    }
                    i += token_characters;
                    newline = true;

                    current_subline++;
                } else {
                    newline = true;
                    // i is not advanced but for the newline cur_line_len will be 0
                    // in the next round so the previous case will catch on if it
                    // doesn't fit
          
                    current_subline++;
                }
            }
            if ( newline == true ) {
                line_offsets.push_back( i );
                cur_line_width = 0;

                _real_lines.push_back( std::pair<int,int>( current_line, current_subline ) );

                if ( current_subline == 0 ) {
                    m_unwrapped_line_offsets.push_back( i );
                }
            }
        }
    }

    m_current_line_limit = line_limit_width;
}

int TextStorageString::getMaxLineWidth() const
{
  int max_line_len = 0, l, i;

  for ( i = 0; i < getNrOfLines(); i++ ) {
    l = getLineWidth( i );
    if ( l > max_line_len ) max_line_len = l;
  }
  
  return max_line_len;
}

int TextStorageString::getLineWidth( int line_nr ) const
{
  size_t start_offset, line_len;  // remember offset from line_offsets are not necessarly inside buffer
  int width = -1;

  if ( ( line_nr < 0 ) || ( line_nr >= getNrOfLines() ) ) return -1;

  if ( (int)m_line_width_cached.size() > line_nr ) {
      width = m_line_width_cached[line_nr];
  }

  if ( width < 0 ) {
      start_offset = line_offsets[ line_nr ];
      if ( ( line_nr + 1 ) < getNrOfLines() ) {
          // next line available
          line_len = line_offsets[ line_nr + 1 ] - start_offset;
          
          // if only newline results in new lines one can sub 1 above
          // but for word wrap it is possible to have a new line
          // without newline
          if ( ( line_len > 0 ) &&
               ( buffer[ start_offset + line_len - 1 ] == '\n' ) )
              line_len--;
      } else {
          // no next line
          line_len = buffer.length() - start_offset;
      }
      width = m_lencalc->getWidth( buffer.c_str() + start_offset, line_len );

      if ( (int)m_line_width_cached.size() <= line_nr ) {
          m_line_width_cached.resize( line_nr + 1, -1 );
      }
      m_line_width_cached[line_nr] = width;
  }

  return width;
}

unsigned int TextStorageString::getTokenLen( unsigned int start_offset, int limit )
{
    unsigned int token_len;
    size_t l;

    if ( start_offset >= buffer.length() ) return 0;
  
    for ( token_len = 0; start_offset < buffer.length(); ) {
        if ( limit > 0 && token_len >= (unsigned int)limit ) break;
        if ( buffer[start_offset] == ' ' ) break;
        if ( buffer[start_offset] == '\n' ) break;
    
        l = UTF8::getLenOfCharacter( buffer.c_str() + start_offset );
        start_offset += l;
        token_len += l;
        if ( l < 1 ) break;
    }
  
    return token_len;
}

void TextStorageString::setLineLimit( int new_limit )
{
    if ( new_limit == m_current_line_limit ) return;
    
    if ( new_limit < -1 ) new_limit = -1;

    initBuffer( new_limit );
}

int TextStorageString::getLineLimit()
{
    return m_current_line_limit;
}

int TextStorageString::getLineCharacters( int line_nr ) const
{
  size_t start_offset, line_len;  // remember offset from line_offsets are not necessarly inside buffer

  if ( ( line_nr < 0 ) || ( line_nr >= getNrOfLines() ) ) return -1;

  start_offset = line_offsets[ line_nr ];
  if ( ( line_nr + 1 ) < getNrOfLines() ) {
    // next line available
    line_len = line_offsets[ line_nr + 1 ] - start_offset;

    // if only newline results in new lines one can sub 1 above
    // but for word wrap it is possible to have a new line
    // without newline
    if ( ( line_len > 0 ) &&
	 ( buffer[ start_offset + line_len - 1 ] == '\n' ) )
      line_len--;
    return line_len;
  } else {
    // no next line
    return buffer.length() - start_offset;
  }
}

std::pair<int,int> TextStorageString::getRealLinePair( int line_nr ) const
{
  if ( line_nr < 0 || line_nr >= (int)_real_lines.size() ) return std::pair<int,int>( -1, -1);
  return _real_lines[line_nr];
}

AWidth &TextStorageString::getAWidth()
{
  return *m_lencalc;
}

void TextStorageString::setAWidth( const RefCount<AWidth> &lencalc )
{
    m_lencalc = lencalc;

    initBuffer( m_current_line_limit );
}

int TextStorageString::findLineNr( std::pair<int,int> real_line ) const
{
  int s = (int)_real_lines.size();
  if ( real_line.first < 0 || real_line.first >= s ) return 0;

  int l = real_line.first;
  for ( ; l < s; l++ ) {
    if ( _real_lines[l] == real_line )
      break;
  }
  if ( l >= s ) return 0;
  return l;
}

int TextStorageString::getNrOfUnwrappedLines() const
{
    return m_unwrapped_line_offsets.size();
}

int TextStorageString::getUnwrappedLine( int line_nr,
                                         unsigned int offset,
                                         int len,
                                         std::string &return_line,
                                         bool raw ) const
{
    size_t start_offset, line_len;  // remember offset from line_offsets are not necessarly inside buffer
    
    if ( line_nr < 0 || line_nr >= getNrOfUnwrappedLines() ) return 1;
    
    start_offset = m_unwrapped_line_offsets[ line_nr ];
    line_len = getUnwrappedLineCharacters( line_nr );
    
    return_line = "";
    
    if ( offset < line_len ) {
        // offset given still inside line
        
        if ( UTF8::isValidCharacter( buffer.c_str() + start_offset + offset ) == false ) {
            int n = (int)offset;
            UTF8::movePosToPrevChar( buffer.c_str() + start_offset, n );
            offset = (unsigned int )n;
        }
        
        start_offset += offset;
        line_len -= offset;
        
        if ( len >= 0 ) {
            // line limiter given
            if ( (unsigned int )len < line_len ) line_len = (unsigned int)len;
        }

        if ( m_convert != NO_CHARACTER_CONVERSION && raw ) {
            if ( line_len > 0 ) {
                size_t raw_start = getRawPosition( start_offset );

                size_t raw_end = getRawPosition( start_offset + line_len );

                return_line.append( m_raw_buffer, raw_start, raw_end - raw_start );
            }
        } else {
            if ( ( start_offset < buffer.length() ) && ( line_len > 0 ) ) {
                return_line.append( buffer, start_offset, line_len );
            }
        }
    }
    return 0;
}

int TextStorageString::getUnwrappedLine( int line_nr,
                                         unsigned int offset,
                                         int len,
                                         std::string &return_line ) const
{
    return getUnwrappedLine( line_nr, offset,
                             len,
                             return_line, false );
}

int TextStorageString::getUnwrappedLine( int line_nr,
                                         unsigned int offset,
                                         std::string &return_line ) const
{
    return getUnwrappedLine( line_nr, offset, -1, return_line );
}

int TextStorageString::getUnwrappedLineRaw( int line_nr,
                                            unsigned int offset,
                                            int len,
                                            std::string &return_line ) const
{
    return getUnwrappedLine( line_nr, offset,
                             len,
                             return_line, true );
}

int TextStorageString::getUnwrappedLineRaw( int line_nr,
                                            unsigned int offset,
                                            std::string &return_line ) const
{
    return getUnwrappedLineRaw( line_nr, offset, -1, return_line );
}

int TextStorageString::getUnwrappedLineCharacters( int line_nr ) const
{
    size_t start_offset, line_len;  // remember offset from line_offsets are not necessarly inside buffer
    
    if ( line_nr < 0 || line_nr >= getNrOfUnwrappedLines() ) return -1;
    
    start_offset = m_unwrapped_line_offsets[ line_nr ];
    if ( ( line_nr + 1 ) < getNrOfUnwrappedLines() ) {
        // next line available
        line_len = m_unwrapped_line_offsets[ line_nr + 1 ] - start_offset;
        
        return line_len - 1; // minus 1 for backslash n
    } else {
        // no next line
        return buffer.length() - start_offset;
    }
}

std::pair< std::pair< int, int >, int > TextStorageString::getLineForOffset( int unwrapped_line_nr, int line_offset ) const
{
    if ( unwrapped_line_nr < 0 || unwrapped_line_nr >= getNrOfUnwrappedLines() ) {
        return std::pair< std::pair< int, int >, int>( std::pair<int, int>( -1, -1 ), -1 );
    }

    if ( m_current_line_limit < 1 ) {
        // no wrapping so it's just the original offset in line (unwrapped_line_nr,0)
        int l = getLineCharacters( unwrapped_line_nr );
        if ( line_offset <= l ) {
            return std::pair< std::pair< int, int >, int>( std::pair<int, int>( unwrapped_line_nr, 0 ), line_offset );
        } else {
            return std::pair< std::pair< int, int >, int>( std::pair<int, int>( -1, -1 ), -1 );
        }
    }

    int line_nr;
    int current_line_offset = 0;

    line_nr = findLineNr( std::pair<int,int>( unwrapped_line_nr, 0 ) );
    while ( line_nr < (int)_real_lines.size() && _real_lines[line_nr].first == unwrapped_line_nr ) {
        int l = getLineCharacters( line_nr );

        if ( line_offset >= current_line_offset && line_offset < ( current_line_offset + l ) ) break;

        if ( ( line_nr + 1 ) >= (int)_real_lines.size() ||
             _real_lines[line_nr + 1].first != unwrapped_line_nr ) {
            break;
        }

        line_nr++;
        current_line_offset += l;
    }
    if ( line_nr < (int)_real_lines.size() && _real_lines[line_nr].first == unwrapped_line_nr ) {
        return std::pair< std::pair< int, int >, int>( _real_lines[line_nr], line_offset - current_line_offset );
    } else {
        return std::pair< std::pair< int, int >, int>( std::pair<int, int>( -1, -1 ), -1 );
    }
}

void TextStorageString::setContent( const std::string &content )
{
    m_raw_buffer = content;
    buffer = content;

    if ( m_convert != NO_CHARACTER_CONVERSION ) {
        convertRawData();
    }

    initBuffer( m_current_line_limit );
}

std::string TextStorageString::getReplacementString( unsigned char ch, size_t column ) const
{
    if ( ch == '\t' ) {
        return std::string( 8 - column % 8, ' ' );
    }

    if ( m_convert == CONVERT_NON_PRINTABLE_HEX ) {
        return AGUIXUtils::formatStringToString( "<0x%02x>", ch );
    }

    return ".";    
}

void TextStorageString::convertRawData()
{
    if ( m_convert == NO_CHARACTER_CONVERSION ) return;

    size_t len = m_raw_buffer.length();
    size_t pos = 0;
    const char *cstr = m_raw_buffer.c_str();

    buffer = "";
    m_converted_pos_to_raw_snapshots.clear();

    uint8_t column = 0;

    while ( pos < len ) {
        storePosToRaw( pos, buffer.size(), column );

        char buf[128];
        size_t buf_rem = sizeof( buf );
        char *out_buf = buf;
        int res = convert_symbol( &cstr[pos], &column,
                                  &out_buf, &buf_rem );
        if ( res < 0 ) break;

        buffer.append( buf, sizeof( buf ) - buf_rem );
        
        pos += res;
    }
}

void TextStorageString::setConvertMode( enum text_storage_converion convert )
{
    m_convert = convert;

    if ( m_convert != NO_CHARACTER_CONVERSION ) {
        convertRawData();
    } else {
        buffer = m_raw_buffer;
    }

    initBuffer( m_current_line_limit );
}

size_t TextStorageString::getRawPosition( const size_t pos ) const
{
    std::tuple< size_t, size_t, uint8_t > current = { 0, 0, 0 };

    for ( const auto &e : m_converted_pos_to_raw_snapshots ) {
        if ( std::get<0>( e ) <= pos ) {
            current = e;
        } else {
            break;
        }
    }

    // for fast forward search use previous result if it is nearer than current
    if ( std::get<0>( current ) < std::get<0>( m_last_raw_lookup ) &&
         std::get<0>( m_last_raw_lookup ) <= pos ) {
        current = m_last_raw_lookup;
    }

    if ( std::get<0>( current ) == pos ) {
        return std::get<1>( current );
    }

    size_t raw_len = m_raw_buffer.length();
    size_t raw_pos = std::get<1>( current );
    const char *raw_str = m_raw_buffer.c_str();

    uint8_t column = std::get<2>( current );

    while ( raw_pos < raw_len ) {
        char buf[128];
        size_t buf_rem = sizeof( buf );
        char *out_buf = buf;
        uint8_t last_column = column;
        int res = convert_symbol( &raw_str[raw_pos], &column,
                                  &out_buf, &buf_rem );
        if ( res < 0 ) break;

        size_t last_pos = std::get<0>( current );

        std::get<0>( current ) += sizeof( buf ) - buf_rem;
        if ( std::get<0>( current ) > pos ) {
            m_last_raw_lookup = std::make_tuple( last_pos, raw_pos, last_column );
            return raw_pos;
        }
        
        raw_pos += res;
    }

    m_last_raw_lookup = std::make_tuple( pos, raw_pos, column );

    return raw_pos;
}

void TextStorageString::storePosToRaw( size_t raw_pos, size_t converted_pos, uint8_t column )
{
    if ( converted_pos >= ( m_converted_pos_to_raw_snapshots.size() + 1 ) * m_pos_to_raw_stepping ) {
        m_converted_pos_to_raw_snapshots.push_back( std::make_tuple( converted_pos, raw_pos, column ) );
    }
}


int TextStorageString::convert_symbol( const char *in, uint8_t *column,
                                       char **out, size_t *out_rem ) const
{
    if ( *out_rem < 1 ) return -EINVAL;

    if ( *in >= 0 && *in <= 127 ) {
        if ( std::isprint( *in ) ) {
            *((*out)++) = *in;
            *column = ( *column + 1 ) % 8;
            (*out_rem)--;
            return 1;
        } else if ( *in == '\n' ) {
            *((*out)++) = *in;
            *column = 0;
            (*out_rem)--;
            return 1;
        } else {
            std::string replacement = getReplacementString( *in, *column );
            if ( replacement.length() > *out_rem ) {
                return -ENOSPC;
            }
            for ( size_t i = 0; i < replacement.length(); i++ ) {
                *((*out)++) = replacement[i];
                *column = ( *column + 1 ) % 8;
                (*out_rem)--;
            }
            return 1;
        }
    } else {
        size_t cl = UTF8::getLenOfCharacter( in );

        if ( cl <= 1 ) {
            if ( UTF8::isValidCharacter( in ) ) {
                *((*out)++) = *in;
                *column = ( *column + 1 ) % 8;
                (*out_rem)--;
                return 1;
            } else {
                // invalid
                std::string replacement = getReplacementString( *in, *column );
                if ( replacement.length() > *out_rem ) {
                    return -ENOSPC;
                }
                for ( size_t i = 0; i < replacement.length(); i++ ) {
                    *((*out)++) = replacement[i];
                    *column = ( *column + 1 ) % 8;
                    (*out_rem)--;
                }
                return 1;
            }
        } else {
            if ( cl > *out_rem ) {
                return -ENOSPC;
            }
            size_t res = cl;
            while ( cl > 0 ) {
                *((*out)++) = *in++;
                *column = ( *column + 1 ) % 8;
                (*out_rem)--;
                cl--;
            }

            return res;
        }
    }
}
