------------------------------------------------------------------------------
--                             G N A T C O L L                              --
--                                                                          --
--                     Copyright (C) 2009-2020, AdaCore                     --
--                                                                          --
-- This library is free software;  you can redistribute it and/or modify it --
-- under terms of the  GNU General Public License  as published by the Free --
-- Software  Foundation;  either version 3,  or (at your  option) any later --
-- version. This library is distributed in the hope that it will be useful, --
-- but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN- --
-- TABILITY or FITNESS FOR A PARTICULAR PURPOSE.                            --
--                                                                          --
-- As a special exception under Section 7 of GPL version 3, you are granted --
-- additional permissions described in the GCC Runtime Library Exception,   --
-- version 3.1, as published by the Free Software Foundation.               --
--                                                                          --
-- You should have received a copy of the GNU General Public License and    --
-- a copy of the GCC Runtime Library Exception along with this program;     --
-- see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see    --
-- <http://www.gnu.org/licenses/>.                                          --
--                                                                          --
------------------------------------------------------------------------------

with Ada.Unchecked_Conversion;
with Interfaces.C;             use Interfaces.C;
with Interfaces.C.Strings;     use Interfaces.C.Strings;
with System.Address_Image;
with GNATCOLL.Traces;          use GNATCOLL.Traces;

package body GNATCOLL.SQL.Sqlite.Gnade is
   Me : constant Trace_Handle := Create ("SQL.SQLITE.GNADE", Off);
   Debug : constant Boolean := False;
   --  Low-level debugging enabled. This traces all C functions that are
   --  called, and is used to check whether we use sqlite correctly.

   type Char_Ptr_Array is array (Natural) of chars_ptr;
   type Char_Ptr_Array_Access is access Char_Ptr_Array;
   pragma Convention (C, Char_Ptr_Array_Access);

   function To_Array is new Ada.Unchecked_Conversion
      (System.Address, Char_Ptr_Array_Access);

   type Sqlite3_Config_Option is
     (
      --  Disable all mutexes
      Config_Single_Thread,  --  nil

      --  Cannot share connections among threads
      Config_Multi_Thread,   --  nil

      --  Full serialization done by sqlite3, connections can be used
      --  from multithreads concurrently.
      Config_Serialized,     --  nil

      --  Specifies alternative memory allocation method
      Config_Malloc,         --  sqlite3_mem_methods
      Config_Get_Malloc,     --  sqlite3_mem_methods

      --  Specifies static buffers to use in various cases
      Config_Scratch,        --  void*, int sz, int N
      Config_Page_Cache,     --  void*, int sz, int N
      Config_Heap,           --  void*, int nByte, int min

      --  Should we collect memory allocation statistics (default is True)
      Config_Mem_Status,     --  boolean

      --  Alternative low-level mutexes
      Config_Mutex,          --  sqlite3_mutex_methods
      Config_Get_Mutex,      --  sqlite3_mutex_methods

      Config_Look_Aside,     --  int int
      Config_Pcache,         --  sqlite3_pcache_methods
      Config_Get_Pcache,     --  sqlite3_pcache_methods
      Config_Log,            --  xFunc, void*
      Config_URI);           --  int

   for Sqlite3_Config_Option use
     (Config_Single_Thread => 1,
      Config_Multi_Thread  => 2,
      Config_Serialized    => 3,
      Config_Malloc        => 4,
      Config_Get_Malloc    => 5,
      Config_Scratch       => 6,
      Config_Page_Cache    => 7,
      Config_Heap          => 8,
      Config_Mem_Status    => 9,
      Config_Mutex         => 10,
      Config_Get_Mutex     => 11,
      Config_Look_Aside    => 13,
      Config_Pcache        => 14,
      Config_Get_Pcache    => 15,
      Config_Log           => 16,
      Config_URI           => 17);

   function Sqlite3_Config
      (Option : Sqlite3_Config_Option) return Result_Codes;
   pragma Import (C, Sqlite3_Config, "sqlite3_config");

   ----------
   -- Open --
   ----------

   procedure Open
     (DB       : out Database;
      Filename : String := Open_In_Memory;
      Flags    : Open_Flags := Open_Readwrite or Open_Create;
      Status   : out Result_Codes)
   is
      function Internal
        (Name  : String;
         Db    : access Database;
         Flags : Integer;
         Vfs   : System.Address := System.Null_Address) return Result_Codes;
      pragma Import (C, Internal, "sqlite3_open_v2");

      DB2    : aliased Database;
      Ignored : Result_Codes;
      pragma Unreferenced (Ignored);
   begin
      if Debug then
         Trace (Me, "sqlite3_open_v2");
      end if;

      Status := Internal
        (Filename & ASCII.NUL, DB2'Unchecked_Access, Integer (Flags));

      if Status = Sqlite_OK then
         DB := DB2;
      else
         Close (DB2);
         DB := No_Database;
      end if;
   end Open;

   ---------------
   -- Error_Msg --
   ---------------

   function Error_Msg (DB : Database) return String is
      function Internal (DB : Database) return chars_ptr;
      pragma Import (C, Internal, "sqlite3_errmsg");
   begin
      if Debug then
         Trace (Me, "sqlite3_errmsg");
      end if;

      --  No need to free the result
      return Value (Internal (DB));
   end Error_Msg;

   -----------
   -- Close --
   -----------

   procedure Close
      (DB : Database;
       Finalize_Prepared_Statements : Boolean := True)
   is
      function Internal_Close (DB : Database) return Result_Codes;
      pragma Import (C, Internal_Close, "sqlite3_close_v2");
      --  If there are still unfinalized prepared statement, actual
      --  deallocation will be deferred. This is intended for use with garbage
      --  collected programming languages, which is the case for the
      --  GNATCOLL.SQL package.

      function Next_Stmt
        (DB : Database; After : Statement := No_Statement)
         return Statement;
      pragma Import (C, Next_Stmt, "sqlite3_next_stmt");

      Stmt    : Statement;
      Ignored : Result_Codes;
      pragma Unreferenced (Ignored);
   begin
      if DB /= null then

         if Finalize_Prepared_Statements then
            if Debug then
               Trace (Me, "Finalize prepared statements");
            end if;

            loop
               if Debug then
                  Trace (Me, "sqlite3_next_stmt");
               end if;

               Stmt := Next_Stmt (DB);
               exit when Stmt = No_Statement;
               Finalize (Stmt);
            end loop;
         end if;

         if Debug then
            Trace (Me, "sqlite3_close_v2");
         end if;

         Ignored := Internal_Close (DB);
      end if;
   end Close;

   -------------
   -- Prepare --
   -------------

   procedure Prepare
     (DB     : Database;
      SQL    : String;  --  UTF-8 encoded
      Stmt   : out Statement;
      Status : out Result_Codes)
   is
      function Internal
        (DB    : Database;
         SQL   : String;
         NByte : Integer;
         Stmt  : access Statement;
         Tail  : access System.Address) return Result_Codes;
      pragma Import (C, Internal, "sqlite3_prepare_v2");

      Stmt2 : aliased Statement;
      Tail  : aliased System.Address;
   begin
      if Debug then
         Trace (Me, "sqlite3_prepare_v2");
      end if;

      Status := Internal (DB, SQL, SQL'Length, Stmt2'Access, Tail'Access);
      Stmt := Stmt2;
   end Prepare;

   ----------
   -- Step --
   ----------

   procedure Step
     (Stmt   : in out Statement;
      Status : out Result_Codes)
   is
      function Internal (Stmt : Statement) return Result_Codes;
      pragma Import (C, Internal, "sqlite3_step");
   begin
      if Debug then
         Trace (Me, "sqlite3_step");
      end if;

      Status := Internal (Stmt);
   end Step;

   -----------------
   -- Column_Text --
   -----------------

   function Column_Text (Stmt : Statement; Col : Natural) return String is
      Val : constant chars_ptr := Column_C_Text (Stmt, Col);
   begin
      return Value (Val);
   end Column_Text;

   -----------------
   -- Column_Name --
   -----------------

   function Column_Name (Stmt : Statement; Col : Natural) return String is
      function Internal (Stmt : Statement; Col : Natural) return chars_ptr;
      pragma Import (C, Internal, "sqlite3_column_name");
   begin
      if Debug then
         Trace (Me, "sqlite3_column_name");
      end if;

      return Value (Internal (Stmt, Col));
   end Column_Name;

   ---------------
   -- Get_Table --
   ---------------

   procedure Get_Table
     (DB     : Database;
      SQL    : String;
      Result : out Result_Table;
      Status : out Result_Codes;
      Error  : out chars_ptr)
   is
      function Internal
        (DB      : Database;
         SQL     : String;
         Result  : access System.Address;
         Rows    : access Natural;
         Columns : access Natural;
         Error   : access Interfaces.C.Strings.chars_ptr) return Result_Codes;
      pragma Import (C, Internal, "sqlite3_get_table");

   begin
      if Debug then
         Trace (Me, "sqlite3_get_table");
      end if;

      Status := Internal
        (DB, SQL & ASCII.NUL, Result.Values'Unrestricted_Access,
         Result.Rows'Unrestricted_Access,
         Result.Columns'Unrestricted_Access,
         Error'Unrestricted_Access);
   end Get_Table;

   ----------------
   -- Free_Table --
   ----------------

   procedure Free_Table (Result : in out Result_Table) is
      procedure Internal (Result : System.Address);
      pragma Import (C, Internal, "sqlite3_free_table");
   begin
      if Debug then
         Trace (Me, "sqlite3_free_table");
      end if;

      Internal (Result.Values);
   end Free_Table;

   ---------------
   -- Get_Value --
   ---------------

   function Get_Value
     (Result : Result_Table;
      Row, Column : Natural) return Interfaces.C.Strings.chars_ptr
   is
      Val : constant Char_Ptr_Array_Access := To_Array (Result.Values);
   begin
      --  First row is for column names, so skip it
      return Val ((Row * 1) * Result.Columns + Column);
   end Get_Value;

   --------------
   -- Get_Rows --
   --------------

   function Get_Rows (Result : Result_Table) return Natural is
   begin
      return Result.Rows;
   end Get_Rows;

   -----------------
   -- Get_Columns --
   -----------------

   function Get_Columns (Result : Result_Table) return Natural is
   begin
      return Result.Columns;
   end Get_Columns;

   ---------------------
   -- Get_Column_Name --
   ---------------------

   function Get_Column_Name
     (Result : Result_Table; Column : Natural) return String
   is
      Val : constant Char_Ptr_Array_Access := To_Array (Result.Values);
   begin
      return Interfaces.C.Strings.Value (Val (Column));
   end Get_Column_Name;

   -----------
   -- Image --
   -----------

   function Image (DB : Database) return String is
      function Convert is new Ada.Unchecked_Conversion
        (Database, System.Address);
   begin
      return "sqlite=" & System.Address_Image (Convert (DB));
   end Image;

   --------------------
   -- Set_Config_Log --
   --------------------

   procedure Set_Config_Log
     (Func : Logger;
      Data : System.Address := System.Null_Address)
   is
      procedure Sqlite3_Config
        (Option : Sqlite3_Config_Option;
         Func  : Logger;
         Data  : System.Address);
      pragma Import (C, Sqlite3_Config, "sqlite3_config");
   begin
      if Debug then
         Trace (Me, "sqlite3_config");
      end if;

      Sqlite3_Config (Config_Log, Func, Data);
   end Set_Config_Log;

   --------------------------
   -- Set_Config_Memstatus --
   --------------------------

   procedure Set_Config_Memstatus (Collect_Stats : Boolean) is
      procedure Sqlite3_Config
        (Option : Sqlite3_Config_Option;
         Status : Integer);
      pragma Import (C, Sqlite3_Config, "sqlite3_config");
   begin
      if Debug then
         Trace (Me, "sqlite3_config");
      end if;

      Sqlite3_Config (Config_Mem_Status, Boolean'Pos (Collect_Stats));
   end Set_Config_Memstatus;

   -----------------------------
   -- Set_Config_Multi_Thread --
   -----------------------------

   function Set_Config_Multi_Thread return Result_Codes is
   begin
      if Debug then
         Trace (Me, "sqlite3_config(MULTI_THREAD)");
      end if;

      return Sqlite3_Config (Config_Multi_Thread);
   end Set_Config_Multi_Thread;

   ---------------------------
   -- Set_Config_Serialized --
   ---------------------------

   procedure Set_Config_Serialized is
      procedure Sqlite3_Config (Option : Sqlite3_Config_Option);
      pragma Import (C, Sqlite3_Config, "sqlite3_config");
   begin
      if Debug then
         Trace (Me, "sqlite3_config");
      end if;

      Sqlite3_Config (Config_Serialized);
   end Set_Config_Serialized;

   ------------------------------
   -- Set_Config_Single_Thread --
   ------------------------------

   function Set_Config_Single_Thread return Result_Codes is
   begin
      if Debug then
         Trace (Me, "sqlite3_config (SINGLE_THREAD)");
      end if;

      return Sqlite3_Config (Config_Single_Thread);
   end Set_Config_Single_Thread;

   -----------------------
   -- Last_Insert_Rowid --
   -----------------------

   function Last_Insert_Rowid (DB : Database) return Long_Long_Integer is
      function Internal (DB : Database) return Long_Long_Integer;
      pragma Import (C, Internal, "sqlite3_last_insert_rowid");
   begin
      if Debug then
         Trace (Me, "sqlite3_last_insert_rowid");
      end if;
      return Internal (DB);
   end Last_Insert_Rowid;

   -----------
   -- Reset --
   -----------

   function Reset (Stmt : Statement) return Result_Codes is
      function Internal (Stmt : Statement) return Result_Codes;
      pragma Import (C, Internal, "sqlite3_reset");
   begin
      if Debug then
         Trace (Me, "sqlite3_reset");
      end if;
      return Internal (Stmt);
   end Reset;

   --------------
   -- Finalize --
   --------------

   procedure Finalize (Stmt : Statement) is
      procedure Internal (Stmt : Statement);
      pragma Import (C, Internal, "sqlite3_finalize");
   begin
      if Debug then
         Trace (Me, "sqlite3_finalize");
      end if;
      Internal (Stmt);
   end Finalize;

   --------------------
   -- Clear_Bindings --
   --------------------

   procedure Clear_Bindings (Stmt : Statement) is
      procedure Internal (Stmt : Statement);
      pragma Import (C, Internal, "sqlite3_clear_bindings");
   begin
      if Debug then
         Trace (Me, "sqlite3_clear_bindings");
      end if;
      Internal (Stmt);
   end Clear_Bindings;

   -----------------
   -- Bind_Double --
   -----------------

   procedure Bind_Double
     (Stmt : Statement; Index : Integer; Value : Interfaces.C.double)
   is
      procedure Internal
        (Stmt : Statement; Index : Integer; Value : Interfaces.C.double);
      pragma Import (C, Internal, "sqlite3_bind_double");
   begin
      if Debug then
         Trace (Me, "sqlite3_bind_double");
      end if;

      Internal (Stmt, Index, Value);
   end Bind_Double;

   --------------
   -- Bind_Int --
   --------------

   procedure Bind_Int
     (Stmt : Statement; Index : Integer; Value : Interfaces.C.int)
   is
      procedure Internal
        (Stmt : Statement; Index : Integer; Value : Interfaces.C.int);
      pragma Import (C, Internal, "sqlite3_bind_int");
   begin
      if Debug then
         Trace (Me, "sqlite3_bind_int");
      end if;

      Internal (Stmt, Index, Value);
   end Bind_Int;

   ----------------
   -- Bind_Int64 --
   ----------------

   procedure Bind_Int64
     (Stmt : Statement; Index : Integer; Value : Interfaces.C.long)
   is
      procedure Internal
        (Stmt : Statement; Index : Integer; Value : Interfaces.C.long);
      pragma Import (C, Internal, "sqlite3_bind_int64");
   begin
      if Debug then
         Trace (Me, "sqlite3_bind_int64");
      end if;

      Internal (Stmt, Index, Value);
   end Bind_Int64;

   ---------------
   -- Bind_Null --
   ---------------

   procedure Bind_Null (Stmt : Statement; Index : Integer) is
      procedure Internal (Stmt : Statement; Index : Integer);
      pragma Import (C, Internal, "sqlite3_bind_null");
   begin
      if Debug then
         Trace (Me, "sqlite3_bind_null");
      end if;

      Internal (Stmt, Index);
   end Bind_Null;

   ---------------
   -- Bind_Text --
   ---------------

   procedure Bind_Text
     (Stmt : Statement;
      Index : Integer;
      Str : System.Address;
      N_Bytes : Natural;
      Destructor : Text_Destructor := null)
   is
      procedure Internal
        (Stmt : Statement; Index : Integer;
         Str : System.Address; N_Bytes : Natural;
         Destructor : Text_Destructor := null);
      pragma Import (C, Internal, "sqlite3_bind_text");
   begin
      if Debug then
         Trace (Me, "sqlite3_bind_text");
      end if;

      Internal (Stmt, Index, Str, N_Bytes, Destructor);
   end Bind_Text;

   -----------------
   -- Backup_Init --
   -----------------

   function Backup_Init
     (Pdest : Database;      --  destination database handle
      Pdest_Name : String;   --  destination database name
      Psource : Database;    --  source database handle
      Psource_Name : String) --  source database name
      return Sqlite3_Backup
   is
      function Internal
        (Pdest : Database;
         PdestName : String;
         Psource : Database;
         PsourceName : String) return Sqlite3_Backup;
      pragma Import (C, Internal, "sqlite3_backup_init");
   begin
      if Debug then
         Trace (Me, "sqlite3_backup_init");
      end if;

      return Internal (Pdest, Pdest_Name & ASCII.NUL,
                       Psource, Psource_Name & ASCII.NUL);
   end Backup_Init;

end GNATCOLL.SQL.Sqlite.Gnade;
