/*
* Copyright (c) 2018-2025 (https://github.com/phase1geo/Minder)
*
* 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 Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* Authored by: Trevor Williams <phase1geo@gmail.com>
*/

using Gtk;
using Gdk;
using Cairo;

public delegate void ConfirmationDialogFunc();

public class Utils {

  //-------------------------------------------------------------
  // Determines if we are executing this within the Pantheon desktop.
  // If we are, we need to use icons in the header bar that contain
  // color.
  public static bool on_elementary() {
    var desktop = Environment.get_variable( "XDG_CURRENT_DESKTOP" );
    return( desktop == "Pantheon" );
  }

  //-------------------------------------------------------------
  // Determines if we are using the adwaita theme.
  public static bool using_adwaita() {
    var settings = Gtk.Settings.get_default();
    var theme    = settings.gtk_theme_name;
    return( (theme != null) && theme.contains( "Adwaita" ) );
  }

  //-------------------------------------------------------------
  // Returns the system information about how this application was
  // built.
  public static string get_flatpak_runtime() {

    // Determine the Flatpak runtime being used
    try {
      var keyfile = new GLib.KeyFile();
      keyfile.load_from_file( "/.flatpak-info", GLib.KeyFileFlags.NONE );
      var runtime = keyfile.get_string( "Application", "runtime" );
      return( "Flatpak Runtime: %s".printf( runtime ) );
    } catch( Error e ) {}

    return( "" );

  }

  //-------------------------------------------------------------
  // Creates the given directory (and all parent directories) with
  // appropriate permissions
  public static bool create_dir( string path ) {
    return( DirUtils.create_with_parents( path, 0755 ) == 0 );
  }

  //-------------------------------------------------------------
  // Returns a regular expression useful for parsing clickable URLs.
  public static string url_re() {
    string[] res = {
      "mailto:.+@[a-z0-9-]+\\.[a-z0-9.-]+",
      "[a-zA-Z0-9]+://[a-z0-9-]+[a-z0-9.-]+(?:/|(?:/[][a-zA-Z0-9!#$%&'*+,.:;=?@_~-]+)*)",
      "file:///([^,\\/:*\\?\\<>\"\\|]+(/|\\\\){0,1})+"
    };
    return( "(" + string.joinv( "|",res ) + ")" );
  }

  //-------------------------------------------------------------
  // Helper function for converting an RGBA color value to a
  // stringified color that can be used by a markup parser.
  public static string color_from_rgba( RGBA rgba ) {
    return( "#%02x%02x%02x".printf( (int)(rgba.red * 255), (int)(rgba.green * 255), (int)(rgba.blue * 255) ) );
  }

  //-------------------------------------------------------------
  // Returns the RGBA color for the given color value
  public static RGBA color_from_string( string value ) {
    RGBA c = {(float)1.0, (float)1.0, (float)1.0, (float)1.0};
    c.parse( value );
    return( c );
  }

  //-------------------------------------------------------------
  // Sets the context source color to the given color value
  public static void set_context_color( Context ctx, RGBA color ) {
    ctx.set_source_rgba( color.red, color.green, color.blue, color.alpha );
  }

  //-------------------------------------------------------------
  // Sets the context source color to the given color value
  // overriding the alpha value with the given value.
  public static void set_context_color_with_alpha( Context ctx, RGBA color, double alpha ) {
    ctx.set_source_rgba( color.red, color.green, color.blue, alpha );
  }

  //-------------------------------------------------------------
  // Returns the red, green and blue color values that are needed
  // by the Pango color attributes
  public static void get_attribute_color( RGBA color, out uint16 red, out uint16 green, out uint16 blue ) {
    var maxval = 65535;
    red   = (uint16)(color.red   * maxval);
    green = (uint16)(color.green * maxval);
    blue  = (uint16)(color.blue  * maxval);
  }

  //-------------------------------------------------------------
  // Checks the given string to see if it is a match to the given
  // pattern.  If it is, the matching portion of the string
  // appended to the list of matches.
  //
  // See : https://valadoc.org/glib-2.0/string.substring.html
  public static string match_string( string pattern, string value) {
    int pattern_byte_idx = value.casefold().index_of( pattern );
    if( pattern_byte_idx != -1 ) {
      unichar  c = 0;
      int i = 0;
      int current_index = pattern_byte_idx;
      while (value.get_prev_char(ref current_index, out c) && i < 10) {
        i++;
      }
      int start = i < 10 ? 0 : current_index;
      i = 0;
      current_index = pattern_byte_idx + pattern.length;
      while (value.get_next_char(ref current_index, out c) && i < 10) {
        i++;
      }
      int end = i < 10 ? -1 : current_index - ( pattern_byte_idx + pattern.length );
      string str = (start > 0 ? "..." : "") +
        value.substring(start, pattern_byte_idx - start) + "<u>" + pattern + "</u>" +
        value.substring(pattern_byte_idx + pattern.length);
      return str;
    }
    return "";
  }

  //-------------------------------------------------------------
  // Returns the rootname of the given filename
  public static string rootname( string filename ) {
    var basename = GLib.Path.get_basename( filename );
    var parts    = basename.split( "." );
    if( parts.length > 2 ) {
      return( string.joinv( ".", parts[0:parts.length-1] ) );
    } else {
      return( parts[0] );
    }
  }

  //-------------------------------------------------------------
  // Returns true if the given coordinates are within the specified
  // bounds
  public static bool is_within_bounds( double x, double y, double bx, double by, double bw, double bh ) {
    return( (bx < x) && (x < (bx + bw)) && (by < y) && (y < (by + bh)) );
  }

  //-------------------------------------------------------------
  // Returns a string that is used to display a tooltip with
  // displayed accelerator.
  public static string tooltip_with_accel( string tooltip, string accel ) {
    string[] accels = {accel};
    return( Granite.markup_accel_tooltip( accels, tooltip ) );
  }

  //-------------------------------------------------------------
  // Opens the given URL in the proper external default application
  public static void open_url( string url ) {
    try {
      AppInfo.launch_default_for_uri( url, null );
    } catch( GLib.Error e ) {
      stdout.printf( _( "error: %s\n" ), e.message );
    }
  }

  //-------------------------------------------------------------
  // Prepares the given note string for use in a markup tooltip.
  public static string prepare_note_markup( string note ) {
    var str = markdown_to_html( note );  // .replace( "<", "&lt;" ).replace( ">", "&gt;" ) );
    // stdout.printf( "---------------\n%s--------------\n", str );
    try {
      MatchInfo match_info;
      var re    = new Regex( "</?(\\w+)[^>]*>" );
      var start = 0;
      var list  = new Queue<int>();
      while( re.match_full( str, -1, start, 0, out match_info ) ) {
        int tag_start, tag_end;
        int name_start, name_end;
        match_info.fetch_pos( 0, out tag_start, out tag_end );
        match_info.fetch_pos( 1, out name_start, out name_end );
        var old_tag = str.slice( tag_start, tag_end );
        var new_tag = old_tag;
        var name    = str.slice( name_start, name_end );
        var end_tag = (str.substring( (name_start - 1), 1 ) == "/");
        switch( name ) {
          case "h1"     :  new_tag = end_tag ? "</span>" : "<span weight=\"bold\" size=\"xx-large\">";  break;
          case "h2"     :  new_tag = end_tag ? "</span>" : "<span weight=\"bold\" size=\"x-large\">";   break;
          case "h3"     :  new_tag = end_tag ? "</span>" : "<span weight=\"bold\" size=\"large\">";     break;
          case "h4"     :
          case "h5"     :
          case "h6"     :  new_tag = end_tag ? "</span>" : "<span weight=\"bold\" size=\"medium\">";    break;
          case "strong" :  new_tag = end_tag ? "</b>" : "<b>";  break;
          case "em"     :  new_tag = end_tag ? "</i>" : "<i>";  break;
          case "code"   :  new_tag = end_tag ? "</tt>" : "<tt>";  break;
          case "blockquote" :  new_tag = end_tag ? "</i>" : "<i>";  break;
          case "hr"     :  new_tag = end_tag ? "" : "---";  break;
          case "p"      :  new_tag = "";  break;
          case "br"     :  new_tag = "";  break;
          case "ul"     :
          case "ol"     :
            new_tag = ""; 
            if( end_tag ) {
              list.pop_tail();
            } else {
              list.push_tail( (name == "ul") ? 1000000 : 1 );
            }
            break;
          case "li"     :
            if( end_tag ) {
              new_tag = "";
            } else {
              var val    = list.pop_tail();
              var prefix = string.nfill( list.get_length(), ' ' );
              if( val >= 1000000 ) {
                new_tag = "%s* ".printf( prefix );
              } else {
                new_tag = "%s%d. ".printf( prefix, val );
              }
              list.push_tail( val + 1 );
            }
            break;
        }
        str   = str.splice( tag_start, tag_end, new_tag );
        start = tag_end + (new_tag.length - old_tag.length);
      }
    } catch( RegexError e ) {
      return( note.replace( "<", "&lt;" ) );
    }
    return( str.replace( "\n\n\n", "\n\n" ) );
  }

  //-------------------------------------------------------------
  // Converts the given Markdown into HTML.
  public static string markdown_to_html( string md, string? tag = null ) {
    string html;
#if MD30
    html = Markup.escape_text( md );
#else
    uint32 flags = Markdown.DocumentFlags.NOPANTS |
                Markdown.DocumentFlags.TOC |
                Markdown.DocumentFlags.@1_COMPAT |
                Markdown.DocumentFlags.AUTOLINK |
                Markdown.DocumentFlags.EXTRA_FOOTNOTE |
                Markdown.DocumentFlags.NOSTYLE |
                Markdown.DocumentFlags.DLEXTRA |
                Markdown.DocumentFlags.FENCEDCODE |
                Markdown.DocumentFlags.IDANCHOR |
                Markdown.DocumentFlags.LATEX;
    var mkd = new Markdown.Document.from_gfm_string( md.data, flags );
    mkd.compile( flags );
    mkd.document( out html );
#endif
    if( tag == null ) {
      return( html );
    } else {
      return( "<" + tag + ">" + html + "</" + tag + ">" );
    }
  }

  //-------------------------------------------------------------
  // Returns the line height of the first line of the given pango
  // layout.
  public static double get_line_height( Pango.Layout layout ) {
    int height;
    var line = layout.get_line_readonly( 0 );
    if( line == null ) {
      int width;
      layout.get_size( out width, out height );
    } else {
      Pango.Rectangle ink_rect, log_rect;
      line.get_extents( out ink_rect, out log_rect );
      height = log_rect.height;
    }
    return( height / Pango.SCALE );
  }

  //-------------------------------------------------------------
  // Searches for the beginning or ending word.
  public static int find_word( string str, int cursor, bool wordstart ) {
    try {
      MatchInfo match_info;
      var substr = wordstart ? str.substring( 0, cursor ) : str.substring( cursor );
      var re = new Regex( wordstart ? ".*(\\W\\w|[\\w\\s][^\\w\\s])" : "(\\w\\W|[^\\w\\s][\\w\\s])" );
      if( re.match( substr, 0, out match_info ) ) {
        int start_pos, end_pos;
        match_info.fetch_pos( 1, out start_pos, out end_pos );
        return( wordstart ? (start_pos + 1) : (cursor + start_pos + 1) );
      }
    } catch( RegexError e ) {}
    return( -1 );
  }

  //-------------------------------------------------------------
  // Returns true if the given string is a valid URL.
  public static bool is_url( string str ) {
    return( Regex.match_simple( url_re(), str ) );
  }

  //-------------------------------------------------------------
  // Returns true if the given file is read-only.
  public static bool is_read_only( string fname ) {
    var file = File.new_for_path( fname );
    var src  = new GtkSource.File();
    src.set_location( file );
    src.check_file_on_disk();
    return( src.is_readonly() );
  }

  //-------------------------------------------------------------
  // Sets the default folder in the given file dialog to the
  // last directory used.
  public static void set_chooser_folder( FileDialog dialog ) {
    var dir = Minder.settings.get_string( "last-directory" );
    if( dir != "" ) {
      var file = File.new_for_path( dir );
      dialog.set_initial_folder( file );
    }
  }

  //-------------------------------------------------------------
  // Stores the last used directory in GLib settings.
  public static void store_chooser_folder( string file, bool is_dir ) {
    var dir = is_dir ? file : GLib.Path.get_dirname( file );
    Minder.settings.set_string( "last-directory", dir );
  }

  //-------------------------------------------------------------
  // Returns the child widget at the given index of the parent
  // widget (or null if one does not exist)
  public static Widget? get_child_at_index( Widget parent, int index ) {
    var child = parent.get_first_child();
    while( (child != null) && (index-- > 0) ) {
      child = child.get_next_sibling();
    }
    return( child );
  }

  //-------------------------------------------------------------
  // Returns the index of the given child within the parent
  public static int get_child_index( Widget parent, Widget child ) {
    var index = 0;
    var current_child = parent.get_first_child();
    while( (current_child != null) && (child != current_child) ) {
      current_child = current_child.get_next_sibling();
      index++;
    }
    return( (current_child == null) ? -1 : index );
  }

  //-------------------------------------------------------------
  // Creates a file chooser dialog and returns it to the code
  public static Gtk.FileDialog make_file_chooser( string user_title, string user_accept_label ) {
      
    var gtk_settings = Gtk.Settings.get_default();

    var use_header = gtk_settings.gtk_dialogs_use_header;
    gtk_settings.gtk_dialogs_use_header = true;
   
    var dialog = new FileDialog() {
      title = user_title,
      accept_label = user_accept_label
    };
    gtk_settings.gtk_dialogs_use_header = use_header;
  
    return( dialog );
    
  }

  //-------------------------------------------------------------
  // Clears the given box widget
  public static void clear_box( Box box ) {
    while( box.get_first_child() != null ) {
      box.remove( box.get_first_child() );
    }
  }

  //-------------------------------------------------------------
  // Clears the given listbox widget
  public static void clear_listbox( ListBox box ) {
    box.remove_all();
  }

  //-------------------------------------------------------------
  // Reads the string from the input stream
  public static string read_stream( InputStream stream ) {
    var str = "";
    var dis = new DataInputStream( stream );
    try {
      do {
        var line = dis.read_line();
        if( line != null ) {
          str += line + "\n";
        }
      } while( dis.get_available() > 0 );
    } catch( IOError e ) {
      return( "" );
    }
    return( str );
  }

  //-------------------------------------------------------------
  // Creates a pixbuf from a Texture
  public static Gdk.Pixbuf? texture_to_pixbuf( Gdk.Texture texture ) {

    FileIOStream iostream;

    try {
      var tmp = File.new_tmp( null, out iostream );
      texture.save_to_png( tmp.get_path() );
      var pixbuf = new Pixbuf.from_file( tmp.get_path() );
      return( pixbuf );
    } catch( Error e ) {
      return( null );
    }

  }

  //-------------------------------------------------------------
  // Draws a rounded rectangle on the given context
  public static void draw_rounded_rectangle( Cairo.Context ctx, double x, double y, double w, double h, double radius ) {

    var deg = Math.PI / 180.0;

    ctx.new_sub_path();
    ctx.arc( (x + w - radius), (y + radius),     radius, (-90 * deg), (0 * deg) );
    ctx.arc( (x + w - radius), (y + h - radius), radius, (0 * deg),   (90 * deg) );
    ctx.arc( (x + radius),     (y + h - radius), radius, (90 * deg),  (180 * deg) );
    ctx.arc( (x + radius),     (y + radius),     radius, (180 * deg), (270 * deg) );
    ctx.close_path();

  }

  //-------------------------------------------------------------
  // Returns true if the given widget should be using a dark mode.
  public static bool use_dark_mode( Widget w ) {
    var color = Utils.color_from_rgba( Granite.contrasting_foreground_color( w.get_color() ) );
    return( color == "#000000" );
  }

  //-------------------------------------------------------------
  // Creates a confirmation dialog window.  If the user accepts
  // message, executes function.
  public static void create_confirmation_dialog( Gtk.Window win, string message, string detail, ConfirmationDialogFunc confirm_func ) {

    var dialog = new Granite.MessageDialog.with_image_from_icon_name(
      message,
      detail,
      "dialog-warning",
      ButtonsType.NONE
    );

    var no = new Button.with_label( _( "No" ) );
    dialog.add_action_widget( no, ResponseType.CANCEL );

    var yes = new Button.with_label( _( "Yes" ) );
    yes.add_css_class( Granite.STYLE_CLASS_SUGGESTED_ACTION );
    dialog.add_action_widget( yes, ResponseType.ACCEPT );

    dialog.set_transient_for( win );
    dialog.set_default_response( ResponseType.CANCEL );
    dialog.set_title( "" );

    dialog.response.connect((id) => {
      if( id == ResponseType.ACCEPT ) {
        confirm_func();
      }
      dialog.close();
    });

    dialog.present();

  }

  //-------------------------------------------------------------
  // Compares to version strings.  Returns 0 if they match, returns
  // -1 if version A is less than version B, or returns 1 if version A
  // is less than version B.
  public static int compare_versions( string a, string b ) {

    string[] parts_a = a.split( "." );
    string[] parts_b = b.split( "." );

    int len = int.max( parts_a.length, parts_b.length );

    for( int i=0; i<len; i++ ) {
      // Missing parts are treated as 0
      int val_a = (i < parts_a.length) ? int.parse( parts_a[i] ) : 0;
      int val_b = (i < parts_b.length) ? int.parse( parts_b[i] ) : 0;

      if (val_a < val_b) {
        return -1;
      } else if (val_a > val_b) {
        return 1;
      }
    }

    return 0;

  }

  //-------------------------------------------------------------
  // Causes the given switcher to adjust tab widths to be the same
  // based on the largest tab.
  public static void set_switcher_tab_widths( Widget switcher ) {

    Widget child;
    var    i = 0;
    uint   max_width = 0;
    int    min, nat, min_bl, nat_bl;

    // Force creation of child buttons
    switcher.show ();

    child = Utils.get_child_at_index( switcher, i++ );
    while( child != null ) {
      child.measure( Orientation.HORIZONTAL, -1, out min, out nat, out min_bl, out nat_bl );
      max_width = uint.max( max_width, (uint)nat );
      child     = Utils.get_child_at_index( switcher, i++ );
    }

    i     = 0;
    child = Utils.get_child_at_index( switcher, i++ );
    while( child != null ) {
      child.set_size_request( (int)max_width, -1 );
      child = Utils.get_child_at_index( switcher, i++ );
    }

  }

}
