/*
* Copyright (c) 2018-2026 (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 GLib;
using Gtk;
using Gee;

public delegate void ImageIdFunc( int id );

public class ImageManager {

  //-------------------------------------------------------------
  // Private class used by the image manager to store image information
  private class ImageItem {

    private ImageManager _manager;

    public int    id    { set; get; default = -1; }
    public string uri   { set; get; default = ""; }
    public string ext   { set; get; default = ""; }
    public bool   valid { set; get; default = false; }

    //-------------------------------------------------------------
    // Default constructor
    public ImageItem( ImageManager manager, string uri ) {
      _manager   = manager;
      this.id    = Minder.settings.get_int( "image-id" );
      this.uri   = uri;
      this.ext   = get_extension();
      this.valid = true;
      Minder.settings.set_int( "image-id", (this.id + 1) );
    }

    //-------------------------------------------------------------
    // Loads the item information from given XML node
    public ImageItem.from_xml( ImageManager manager, Xml.Node* n ) {
      _manager = manager;
      string? i = n->get_prop( "id" );
      if( i != null ) {
        id = int.parse( i );
      }
      string? u = n->get_prop( "uri" );
      if( u != null ) {
        uri = u;
      }
      string? e = n->get_prop( "ext" );
      if( e != null ) {
        ext = e;
      }
      valid = true;
    }

    //-------------------------------------------------------------
    // Saves the given image item to the specified XML node
    public void save( Xml.Node* parent ) {
      Xml.Node* n = new Xml.Node( null, "image" );
      n->new_prop( "id",  id.to_string() );
      n->new_prop( "uri", uri );
      n->new_prop( "ext", ext );
      parent->add_child( n );
    }

    //-------------------------------------------------------------
    // Returns true if the file exists
    public bool exists() {
      return( FileUtils.test( get_path(), FileTest.EXISTS ) );
    }

    //-------------------------------------------------------------
    // Returns the extension associated with the filename
    public string get_extension() {
      if( uri != "" ) {
        var parts = uri.split( "." );
        var ext   = parts[parts.length - 1].split( "?" )[0];
        if( (ext == "bmp") || (ext == "png") || (ext == "jpg") || (ext == "jpeg") || (ext == "svg") ) {
          return( "." + ext );
        }
      } else {
        return( ".png" );
      }
      return( "" );
    }

    //-------------------------------------------------------------
    // Returns the full pathname to the given fname
    public string get_path() {
      var basename = "img%06x%s".printf( id, ext );
      return( GLib.Path.build_filename( _manager.get_image_dir(), basename ) );
    }

    //-------------------------------------------------------------
    // Copies the given URI to the given filename in the storage
    // directory
    public bool copy_file() {
      var rfile = File.new_for_uri( uri );
      var lfile = File.new_for_path( get_path() );
      try {
        rfile.copy( lfile, FileCopyFlags.OVERWRITE );
      } catch( Error e ) {
        stdout.printf( "Error: %s\n", e.message );
        return( false );
      }
      return( true );
    }

    //-------------------------------------------------------------
    // If the current item is no longer valid, remove it from the
    // file system
    public void cleanup() {
      if( !valid && exists() ) {
        FileUtils.unlink( get_path() );
      }
    }

  }

  private Array<ImageItem> _images;
  private HashMap<int,int> _id_map;
  private string?          _image_dir = null;

  //-------------------------------------------------------------
  // Default constructor
  public ImageManager() {

    // Create the images directory if it does not exist
    set_image_dir( get_image_dir() );

    // Allocate the images array
    _images = new Array<ImageItem>();
    _id_map = new HashMap<int,int>();

  }

  //-------------------------------------------------------------
  // Returns the web pathname used to store downloaded images
  public string? get_image_dir() {
    return( _image_dir );
  }

  //-------------------------------------------------------------
  // Sets the image dir to the specified path (pass null to use
  // the default system path)
  public void set_image_dir( string? image_dir ) {

    _image_dir = image_dir ?? GLib.Path.build_filename( Environment.get_user_data_dir(), "minder", "images" );

    // Create the images directory if it does not exist
    DirUtils.create_with_parents( _image_dir, 0775 );

  }

  //-------------------------------------------------------------
  // Loads the image manager information from the specified XML node
  public void load( Xml.Node* n ) {
    for( Xml.Node* it = n->children; it != null; it = it->next ) {
      if( it->type == Xml.ElementType.ELEMENT_NODE ) {
        if( it->name == "image" ) {
          var ii = new ImageItem.from_xml( this, it );
          if( !_id_map.has_key( ii.id ) ) {
            _images.append_val( ii );
          }
        }
      }
    }
  }

  //-------------------------------------------------------------
  // Saves the image manager information to the file
  public void save( Xml.Node* n ) {
    for( int i=0; i<_images.length; i++ ) {
      if( _images.index( i ).valid ) {
        _images.index( i ).save( n );
      }
    }
  }

  //-------------------------------------------------------------
  // Searches the list of stored image items, returning the array
  // index of the item that matches.  If no match is found, a
  // value of -1 is returned.
  private ImageItem? find_match( int id ) {
    for( int i=0; i<_images.length; i++ ) {
      if( _images.index( i ).id == id ) {
        return( _images.index( i ) );
      }
    }
    return( null );
  }

  //-------------------------------------------------------------
  // Finds an image item that matches the given URI and returns
  // the index of the matching item.
  private ImageItem? find_uri_match( string uri ) {
    for( int i=0; i<_images.length; i++ ) {
      if( _images.index( i ).uri == uri ) {
        return( _images.index( i ) );
      }
    }
    return( null );
  }

  //-------------------------------------------------------------
  // Adds the given image information to the stored list.  Returns
  // the image ID that the NodeImage class will store to reference
  // the image details.  If the image could not be added, returns
  // a value of -1.
  public int add_image( string uri, int? orig_id = null ) {
    var item = find_uri_match( uri );
    if( item == null ) {
      item = new ImageItem( this, uri );
      if( !item.copy_file() ) return( -1 );
      _images.append_val( item );
    } else if( !item.exists() ) {
      if( !item.copy_file() ) return( -1 );
    }
    if( orig_id != null ) {
      _id_map.set( orig_id, item.id );
    }
    return( item.id );
  }

  //-------------------------------------------------------------
  // Creates a file with the contents of the given pixbuf and
  // adds the file information to the manager list.  Returns the
  // image ID that the NodeImage class will store to reference
  // the image details.  If the image could not be added, returns
  // a value of -1.
  public int add_pixbuf( Gdk.Pixbuf buf, int? orig_id = null ) {
    var item = new ImageItem( this, "" );
    try {
      buf.save( item.get_path(), "png", null );
      _images.append_val( item );
    } catch( Error e ) {
      return( -1 );
    }
    return( item.id );
  }

  //-------------------------------------------------------------
  // Returns the full pathname of the stored file for the given
  // image ID
  public string? get_file( int id ) {
    var item = find_match( id );
    if( item != null ) {
      return( item.get_path() );
    }
    return( null );
  }

  //-------------------------------------------------------------
  // Returns the MIME type associated with the given image file
  public string? get_mime_type( int id ) {
    var item = find_match( id );
    if( item != null ) {
      return( "image/%s".printf( item.get_extension().substring( 1 ) ) );
    }
    return( null );
  }

  //-------------------------------------------------------------
  // Returns the list of stored files
  public Array<int> get_ids() {
    var ids = new Array<int>();
    for( int i=0; i<_images.length; i++ ) {
      ids.append_val( _images.index( i ).id );
    }
    return( ids );
  }

  //-------------------------------------------------------------
  // Returns the stored URI for the given imaged ID
  public string get_uri( int id ) {
    var item = find_match( id );
    if( item != null ) {
      return( item.uri );
    }
    return( "" );
  }

  //-------------------------------------------------------------
  // Sets the validity of the given URI to the the specified value.
  // When an image is no longer needed, this method should be
  // called with a value of false.  When an image is needed again,
  // this method should be called with a value of true.
  public void set_valid( int id, bool value ) {
    var item = find_match( id );
    if( item != null ) {
      item.valid = value;
    }
  }

  //-------------------------------------------------------------
  // Cleans up the contents of the stored images
  public void cleanup() {
    for( int i=0; i<_images.length; i++ ) {
      _images.index( i ).cleanup();
    }
  }

  //-------------------------------------------------------------
  // Returns the ID to use for the given ID
  public int get_id( int id ) {
    if( _id_map.has_key( id ) ) {
      return( _id_map.get( id ) );
    }
    return( id );
  }

  //-------------------------------------------------------------
  // Allows the user to choose an image file.  If the user selects
  // an existing file, adds the image to the manager and returns
  // the image ID to the calling function.  If no image was
  // selected, a value of -1 will be returned.
  public void choose_image( Gtk.Window parent, ImageIdFunc func ) {

    int id = -1;

    var dialog = Utils.make_file_chooser( _( "Select Image" ), _( "Select" ) );
    // Utils.set_chooser_folder( dialog );

    // Allow pixbuf image types
    var filter  = new FileFilter();
    filter.set_filter_name( _( "Images" ) );
    filter.add_pattern( "*.bmp" );
    filter.add_pattern( "*.png" );
    filter.add_pattern( "*.jpg" );
    filter.add_pattern( "*.jpeg" );
    filter.add_pattern( "*.svg" );

    var filters = new GLib.ListStore( typeof(FileFilter) );
    filters.append( filter );
    dialog.set_filters( filters );

    dialog.open.begin( parent, null, (obj, res) => {
      try {
        var file = dialog.open.end( res );
        if( file != null ) {
          id = add_image( file.get_uri() );
          func( id );
        }
      } catch( Error e ) {}
    });

  }

}

