/* Thin wrappers for retaining compatibility for both Qt6.x and Qt5.x */

#pragma once

#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QKeyEvent>
#include <QString>
#include <QTextDocument>
#include <QUrl>
#include <QUrlQuery>
#include <QFileInfo>
#include <QWidget>
#include "filetype.hh"
#include <string>


using std::string;

namespace Utils {

/// Checks if the application is running under a Wayland session.
bool isWayland();
QMap< QString, QString > str2map( const QString & contextsEncoded );

inline bool isCJKChar( ushort ch )
{
  if ( ( ch >= 0x3400 && ch <= 0x9FFF ) || ( ch >= 0xF900 && ch <= 0xFAFF ) || ( ch >= 0xD800 && ch <= 0xDFFF ) )
    return true;

  return false;
}
/**
 * remove right end space
 */
inline QString rstrip( const QString & str )
{
  int n = str.size() - 1;
  for ( ; n >= 0; --n ) {
    if ( !str.at( n ).isSpace() ) {
      return str.left( n + 1 );
    }
  }
  return {};
}

inline uint32_t leadingSpaceCount( const QString & str )
{
  for ( int i = 0; i < str.size(); i++ ) {
    if ( str.at( i ).isSpace() ) {
      continue;
    }
    else {
      return i;
    }
  }
  return 0;
}

inline QString trimQuotes( const QString & str )
{
  const auto * begin = str.cbegin();
  const auto * end   = str.cend();
  while ( begin < end && ( end[ -1 ] == '\'' || end[ -1 ] == '\"' ) ) {
    --end;
  }
  while ( begin < end && ( *begin == '\'' || *begin == '\"' ) ) {
    begin++;
  }
  return str.sliced( begin - str.cbegin(), end - begin );
}

std::string c_string( const QString & str );
bool endsWithIgnoreCase( QByteArrayView str, QByteArrayView extension );
/**
 * remove punctuation , space, symbol
 *
 *
 * " abc, '" should be "abc"
 */
inline QString trimNonChar( const QString & str )
{
  QString remain;
  int n = str.size() - 1;
  for ( ; n >= 0; --n ) {
    auto c = str.at( n );
    if ( !c.isSpace() && !c.isSymbol() && !c.isNonCharacter() && !c.isPunct() && !c.isNull() ) {
      remain = str.left( n + 1 );
      break;
    }
  }

  n = 0;
  for ( ; n < remain.size(); n++ ) {
    auto c = remain.at( n );
    if ( !c.isSpace() && !c.isSymbol() && !c.isNonCharacter() && !c.isPunct() ) {
      return remain.mid( n );
    }
  }

  return "";
}

/**
 * str="abc\r\n\u0000" should be returned as "abc"
 * @brief rstripnull
 * @param str
 * @return
 */
inline QString rstripnull( const QString & str )
{
  int n = str.size() - 1;
  for ( ; n >= 0; --n ) {
    auto c = str.at( n );
    if ( !c.isSpace() && !c.isNull() ) {
      return str.left( n + 1 );
    }
  }
  return "";
}

inline bool isExternalLink( const QUrl & url )
{
  return url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "ftp" || url.scheme() == "mailto"
    || url.scheme() == "file" || url.toString().startsWith( "//" );
}

inline bool isHtmlResources( const QUrl & url )
{
  auto fileName   = url.fileName();
  qsizetype index = fileName.lastIndexOf( "." );
  QStringList extensions{ ".css", ".woff", ".woff2", ".ttf", ".otf", ".bmp", ".jpg",  ".png", ".gif", ".tif",
                          ".wav", ".ogg",  ".oga",   ".mp3", ".mp4", ".aac", ".flac", ".mid", ".wv",  ".ape" };

  if ( index > -1 ) {
    auto ext = fileName.mid( index );
    if ( extensions.contains( ext, Qt::CaseInsensitive ) )
      return true;
  }
  // some url has the form like  https://xxxx/audio?file=***.mp3&a=1 etc links.
  for ( QString extension : extensions ) {
    if ( url.url().contains( extension, Qt::CaseInsensitive ) )
      return true;
  }
  return false;
}

inline QString escape( const QString & plain )
{
  return plain.toHtmlEscaped();
}

// should ignore key event.
inline bool ignoreKeyEvent( QKeyEvent * keyEvent )
{
  if ( keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Backspace || keyEvent->key() == Qt::Key_Tab
       || keyEvent->key() == Qt::Key_Backtab || keyEvent->key() == Qt::Key_Escape )
    return true;
  return false;
}

inline QString json2String( const QJsonObject & json )
{
  return QString( QJsonDocument( json ).toJson( QJsonDocument::Compact ) );
}

inline QString json2String( const QJsonArray & json )
{
  return QString( QJsonDocument( json ).toJson( QJsonDocument::Compact ) );
}

inline QString json2String( const QString & str )
{
  return QString( QJsonDocument( QJsonArray{ str } ).toJson( QJsonDocument::Compact ) ).mid( 1 ).chopped( 1 );
}

inline QStringList repeat( const QString str, const int times )
{
  QStringList list;
  for ( int i = 0; i < times; i++ ) {
    list << str;
  }
  return list;
}

namespace AtomicInt {

inline int loadAcquire( const QAtomicInt & ref )
{
  return ref.loadAcquire();
}

} // namespace AtomicInt

/**
 * Get MIME type for audio file based on file path
 * @param path File path to determine MIME type for
 * @param extension Output parameter for the file extension
 * @return The determined MIME type
 */
QString getAudioMimeType( const QString & path, QString & extension );

namespace Url {

// This wrapper is created due to behavior change of the setPath() method
// See: https://bugreports.qt-project.org/browse/QTBUG-27728
//       https://codereview.qt-project.org/#change,38257
inline QString ensureLeadingSlash( const QString & path )
{
  QLatin1Char slash( '/' );
  if ( path.startsWith( slash ) )
    return path;
  return slash + path;
}

inline bool hasQueryItem( const QUrl & url, const QString & key )
{
  return QUrlQuery( url ).hasQueryItem( key );
}

inline QString queryItemValue( const QUrl & url, const QString & item )
{
  return QUrlQuery( url ).queryItemValue( item, QUrl::FullyDecoded );
}

inline QByteArray encodedQueryItemValue( const QUrl & url, const QString & item )
{
  return QUrlQuery( url ).queryItemValue( item, QUrl::FullyEncoded ).toLatin1();
}

inline void addQueryItem( QUrl & url, const QString & key, const QString & value )
{
  QUrlQuery urlQuery( url );
  urlQuery.addQueryItem( key, value );
  url.setQuery( urlQuery );
}

inline QString getWordFromPathOrHost( const QUrl & url )
{
  auto path = url.path();
  if ( !path.isEmpty() ) {
    return path.startsWith( "/" ) ? path.mid( 1 ) : path;
  }
  auto host = url.host();
  return host != "localhost" ? host : QString();
}

inline void removeQueryItem( QUrl & url, const QString & key )
{
  QUrlQuery urlQuery( url );
  urlQuery.removeQueryItem( key );
  url.setQuery( urlQuery );
}

inline void setQueryItems( QUrl & url, const QList< std::pair< QString, QString > > & query )
{
  QUrlQuery urlQuery( url );
  urlQuery.setQueryItems( query );
  url.setQuery( urlQuery );
}

inline QString path( const QUrl & url )
{
  return url.path( QUrl::FullyDecoded );
}

inline void setFragment( QUrl & url, const QString & fragment )
{
  url.setFragment( fragment, QUrl::DecodedMode );
}

inline QString fragment( const QUrl & url )
{
  return url.fragment( QUrl::FullyDecoded );
}

// get the query word of bword and gdlookup scheme.
// if the scheme is gdlookup or scheme ,the first value of pair is true,otherwise is false;
inline std::pair< bool, QString > getQueryWord( const QUrl & url )
{
  QString word;
  bool validScheme = false;
  if ( url.scheme().compare( "gdlookup" ) == 0 ) {
    validScheme = true;
    if ( hasQueryItem( url, "word" ) ) {
      word = queryItemValue( url, "word" );
    }
    else {
      word = Url::getWordFromPathOrHost( url );
    }
  }
  if ( url.scheme().compare( "bword" ) == 0 || url.scheme().compare( "entry" ) == 0 ) {
    validScheme = true;
    word        = Url::getWordFromPathOrHost( url );
  }
  return std::make_pair( validScheme, word );
}

inline QString getParams( const QUrl & url, const QString & key )
{
  if ( url.scheme().compare( "gdlookup" ) == 0 ) {
    if ( hasQueryItem( url, key ) ) {
      return queryItemValue( url, key );
    }
    return Url::getWordFromPathOrHost( url );
  }
  if ( url.scheme().compare( "bword" ) == 0 || url.scheme().compare( "entry" ) == 0 ) {
    return Url::getWordFromPathOrHost( url );
  }
  return QString();
}

inline bool isAudioUrl( const QUrl & url )
{
  if ( !url.isValid() ) {
    return false;
  }

  // gdau links are known to be audios, (sometimes they may not have file extension).
  if ( url.scheme() == "gdau" || url.scheme() == "gdprg" || url.scheme() == "gdtts" ) {
    return true;
  }

  // Note: we check for forvo sound links explicitly, as they don't have extensions
  return ( url.scheme() == "http" || url.scheme() == "https" )
    && ( Filetype::isNameOfSound( url.path().toUtf8().data() ) || url.host() == "apifree.forvo.com" );
}

inline bool isWebAudioUrl( const QUrl & url )
{
  if ( !url.isValid() ) {
    return false;
  }
  // Note: we check for forvo sound links explicitly, as they don't have extensions

  return ( url.scheme() == "http" || url.scheme() == "https" )
    && ( Filetype::isNameOfSound( url.path().toUtf8().data() ) || url.host() == "apifree.forvo.com" );
}

/// Uses some heuristics to chop off the first domain name from the host name,
/// but only if it's not too base. Returns the resulting host name.
inline QString getHostBase( const QString & host )
{
  QStringList domains = host.split( '.' );

  int left = domains.size();

  // Skip last <=3-letter domain name
  if ( left && domains[ left - 1 ].size() <= 3 ) {
    --left;
  }

  // Skip another <=3-letter domain name
  if ( left && domains[ left - 1 ].size() <= 3 ) {
    --left;
  }

  if ( left > 1 ) {
    // We've got something like www.foobar.co.uk -- we can chop off the first
    // domain

    return host.mid( domains[ 0 ].size() + 1 );
  }
  else {
    return host;
  }
}

inline QString getHostBaseFromUrl( const QUrl & url )
{
  QString host = url.host();

  return getHostBase( host );
}

/// \brief Extract the base domain from a given domain string.
///
/// This method handles several cases:
/// 1. Standard domains like "example.com" -> "example.com"
/// 2. Subdomains like "www.example.com" -> "example.com"
/// 3. Generic patterns like ".com.xx", ".co.xx", ".org.xx" where xx is a 2-3 character TLD -> "example.com"
///
/// Examples:
/// - "www.example.com.jp" -> "example.com"
/// - "subdomain.example.org.uk" -> "example.org"
///
/// \param domain The domain string to process
/// \return The extracted base domain
QString extractBaseDomain( const QString & domain );

QString getSchemeAndHost( const QUrl & url );

} // namespace Url

namespace Path {
QString combine( const QString & path1, const QString & path2 );
}

namespace Widget {
void setNoResultColor( QWidget * widget, bool noResult );
}

namespace Html {
// See Issue #271: A mechanism to clean-up invalid HTML cards.
std::string getHtmlCleaner();
} // namespace Html

/// Utilities to convert a wide string or an utf8 string to the local 8bit
/// encoding of the file system, and to do other manipulations on the file
/// names.
namespace Fs {

using std::string;

/// Returns the filesystem separator (/ on Unix and clones, \ on Windows).
char separator();

/// Returns the name part of the given filename.
string basename( const string & );
void removeDirectory( const QString & directory );

void removeDirectory( const string & directory );

inline QString findFirstExistingFile( std::initializer_list< QString > filePaths )
{
  for ( const QString & filePath : filePaths ) {
    if ( QFileInfo::exists( filePath ) ) {
      return filePath;
    }
  }
  return QString();
}

inline std::string findFirstExistingFile( std::initializer_list< std::string > filePaths )
{
  for ( const std::string & filePath : filePaths ) {
    auto fp = QString::fromStdString( filePath );
    if ( QFileInfo::exists( fp ) ) {
      return filePath;
    }
  }
  return {};
}

inline bool anyExistingFile( std::initializer_list< std::string > filePaths )
{
  for ( const std::string & filePath : filePaths ) {
    auto fp = QString::fromStdString( filePath );
    if ( QFileInfo::exists( fp ) ) {
      return true;
    }
  }
  return false;
}

// used for std::string and char*
inline bool exists( std::string_view filename ) noexcept
{
  return QFileInfo::exists( QString::fromUtf8( filename.data(), filename.size() ) );
}

} // namespace Fs

namespace WebSite {
QString urlReplaceWord( const QString url, QString word );
}

QString escapeAmps( const QString & str );

QString unescapeAmps( const QString & str );

/**
 * Ellipsize a string to a maximum length, adding ellipsis if necessary
 * @param str The string to ellipsize
 * @param maxLength The maximum length of the ellipsized string
 * @return The ellipsized string
 */
QString ellipsizeString( const QString & str, int maxLength );

} // namespace Utils
