/* utf8.cc
 * This file belongs to Worker, a file manager for UN*X/X11.
 * Copyright (C) 2006-2024 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 "utf8.hh"
#include <wchar.h>
#include "lowlevelfunc.h"

#ifdef DISABLE_UTF8_SUPPORT
UTF8::utf8_mode_t UTF8::utf8_mode = UTF8::UTF8_DISABLED;
#else
UTF8::utf8_mode_t UTF8::utf8_mode = UTF8::UTF8_NORMAL;
#endif
UTF8::encodings_enum_t UTF8::cur_enc = UTF8::ENCODING_UNSUPPORTED;
bool UTF8::cur_enc_inited = false;

bool UTF8::isValidCharacter( const char *str )
{
#ifdef DISABLE_UTF8_SUPPORT
  return true;
#else
  size_t l;

  if ( getUTF8Mode() == UTF8_DISABLED )
    return true;

  mbstate_t mbstate;
  memset( &mbstate, 0, sizeof( mbstate ) );
  
  //TODO a better idea would be to a per thread state
  //  but on the other hand, since this function can be called with
  //  any unrelated strings we probably need to clear the state
  //  anyway
  l = mbrlen( str, MB_CUR_MAX, &mbstate );
  if ( l == (size_t)(-1) || l == (size_t)(-2) ) return false;
  return true;
#endif
}

size_t UTF8::getLenOfCharacter( const char *str )
{
#ifdef DISABLE_UTF8_SUPPORT
    if ( str == NULL || *str == '\0' ) return 0;

    return 1;
#else
  size_t l;
  
  if ( str == NULL ) return 0;
  if ( *str == '\0' ) return 0;

  if ( getUTF8Mode() == UTF8_DISABLED ) {
      return 1;
  }

  // optimization for 7-bit characters, in utf8 characters with
  // more than 1 byte start with a highbit char
  if ( (signed char)*str >= 0 ) return 1;
  
  mbstate_t mbstate;
  memset( &mbstate, 0, sizeof( mbstate ) );
  
  //TODO a better idea would be to a per thread state
  //  but on the other hand, since this function can be called with
  //  any unrelated strings we probably need to clear the state
  //  anyway
  l = mbrlen( str, MB_CUR_MAX, &mbstate );
  if ( l == (size_t)(-1) || l == (size_t)(-2) ) l = 1;

  return l;
#endif
}

int UTF8::buildCharacterLookupList( const char *str,
                                    std::vector<int> &list )
{
    return UTF8::buildCharacterLookupListMaxlen( str, -1, list );
}

int UTF8::buildCharacterLookupListMaxlen( const char *str,
                                          int maxlen,
                                          std::vector<int> &list )
{
  size_t l, cur;

  if ( str == NULL ) return -1;

  list.push_back( 0 );
  cur = 0;
  while ( *str != '\0' ) {
    l = getLenOfCharacter( str );

    if ( l < 1 ) break;

    cur += l;
    if ( maxlen >= 0 && (int)cur > maxlen ) {
        break;
    }
    list.push_back( cur );
    str += l;
  }
  
  return 0;
}

UTF8::utf8_mode_t UTF8::getUTF8Mode()
{
  return utf8_mode;
}

void UTF8::disableUTF8()
{
  utf8_mode = UTF8_DISABLED;
}

int UTF8::movePosToNextChar( const char *str, int &pos )
{
    size_t l;

    l = getLenOfCharacter( str + pos );
    pos += l;

    return l;
}

int UTF8::movePosToPrevChar( const char *str, int &pos )
{
    int old_pos = pos;

    pos--;

    while ( pos >= 0 && isValidCharacter( str + pos ) == false ) pos--;

    if ( pos < 0 ) pos = 0;
    return old_pos - pos;
}

int UTF8::findCharacterPosition( int byte_pos, std::vector<int> &char2byte_lookup )
{
  /* this function searches for a value y with char2byte_lookup[y] <= byte_pos and
     char2byte_lookup[y + 1] > byte_pos */
  int left_pos, right_pos, mid_pos;

  left_pos = 0;
  right_pos = char2byte_lookup.size() - 1;

  if ( left_pos >= right_pos ) return 0;
  if ( byte_pos < 0 ) return 0;
  if ( char2byte_lookup[right_pos] <= byte_pos ) return right_pos;

  for (; right_pos - left_pos > 1;) {
    mid_pos = ( left_pos + right_pos ) / 2;
    if ( char2byte_lookup[mid_pos] > byte_pos ) {
      right_pos = mid_pos;
    } else {
      left_pos = mid_pos;
    }
  }

  return left_pos;
}

//TODO this method also counts invalid characters
//  and this is not always wanted
int UTF8::getNumberOfCharacters( const char *str )
{
  if ( str == NULL ) return 0;

  size_t len = strlen( str );
  int chars = 0;
  size_t pos = 0;
  size_t cl;
  
  while ( pos < len ) {
    cl = getLenOfCharacter( str + pos );
    if ( cl < 1 ) break;
    pos += cl;
    chars++;
  }
  return chars;
}

bool UTF8::isValidCharacterString( const char *str )
{
  if ( str == NULL ) return 0;

  size_t len = strlen( str );
  size_t pos = 0;
  size_t cl;
  
  while ( pos < len ) {
    if ( isValidCharacter( str + pos ) == false )
      break;
    cl = getLenOfCharacter( str + pos );
    if ( cl < 1 ) break;
    pos += cl;
  }

  if ( pos == len ) return true;
  return false;
}

UTF8::encodings_enum_t UTF8::checkSupportedEncodings()
{
#ifdef DISABLE_UTF8_SUPPORT
  if ( cur_enc_inited == false ) {
    cur_enc = ENCODING_8BIT;
    cur_enc_inited = true;
  }
  return ENCODING_8BIT;
#else
  size_t e1, e2;
  encodings_enum_t retval = ENCODING_UNSUPPORTED;
  mbstate_t mbstate;

  memset( &mbstate, 0, sizeof( mbstate ) );
  
  e1 = mbrlen( "ä", MB_CUR_MAX, &mbstate );

  // restore again in case the previous mbrlen failed
  memset( &mbstate, 0, sizeof( mbstate ) );

  e2 = mbrlen( "Ã¤", MB_CUR_MAX, &mbstate );

  if ( e1 == (size_t)-1 && e2 == 2 ) {
    // latin1 encoded not recognized
    // utf8 encoded recognized
    // => utf8 supported
    retval = ENCODING_UTF8;
    debugmsg( "Encoding check: UTF8 supported\n");
  } else if ( e1 == 1 && e2 == 1 ) {
    // latin1 encoded recognized
    // utf8 encoded recognized as latin1
    // no utf8 support
    retval = ENCODING_8BIT;
    debugmsg( "Encoding check: 8Bitsupported\n");
  } else {
    // both are not recognized
    // no utf8 and no latin1
    // mh, this sucks so disabled utf8 completed
    // so some encodings work again
    disableUTF8();
    retval = ENCODING_UNSUPPORTED;
    debugmsg( "Encoding check: unsupported\n");
  }

  if ( cur_enc_inited == false ) {
    cur_enc = retval;
    cur_enc_inited = true;
  }
  return retval;
#endif
}

UTF8::encodings_enum_t UTF8::getCurrentEncoding()
{
  if ( cur_enc_inited == false ) {
    return checkSupportedEncodings();
  }
  return cur_enc;
}

std::string UTF8::convertToValidCharacterString( const char *str )
{
    if ( str == NULL ) return "";

    size_t len = strlen( str );
    size_t pos = 0;
    size_t cl;

    std::string res;
  
    while ( pos < len ) {
        if ( isValidCharacter( str + pos ) == false ) {
            res += "\\x";
            unsigned char nibble0 = (unsigned char)str[pos] >> 4;
            unsigned char nibble1 = (unsigned char)str[pos] & 0xf;
            res += nibble0 <= 9 ? ( '0' + nibble0 ) : ( 'a' + nibble0 - 10 );
            res += nibble1 <= 9 ? ( '0' + nibble1 ) : ( 'a' + nibble1 - 10 );
            pos++;
            continue;
        }

        cl = getLenOfCharacter( str + pos );

        while ( cl > 0 && pos < len ) {
            res += str[pos++];
            cl--;
        }
    }

    return res;
}

int UTF8::movePosBackToValidPos( const char *str, int pos )
{
    if ( pos < 0 ) return -1;

    for ( ; pos > 0; pos-- ) {
        char ch = str[ pos ];

        if ( ch == '\0' ) break;

        // high-bit not set is a valid utf8 character and latin1
        if ( ch >= 0 ) break;

#ifndef DISABLE_UTF8_SUPPORT
        if ( UTF8::getUTF8Mode() != UTF8::UTF8_DISABLED && UTF8::getCurrentEncoding() == UTF8::ENCODING_UTF8 ) {
            //TODO
            if ( ( ch & 0xc0 ) == 0xc0 ) {
                // first two high bits set so this is a multibyte lead byte
                break;
            }

            // this is a continuation byte so reduce len by falling through
        } else {
#endif
            // without utf8 support any byte is valid
            break;
#ifndef DISABLE_UTF8_SUPPORT
        }
#endif
    }

    return pos;
}
