-- This file is  free  software, which  comes  along  with  SmartEiffel. This
-- software  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. You can modify it as you want, provided
-- this header is kept unaltered, and a notification of the changes is added.
-- You  are  allowed  to  redistribute  it and sell it, alone or as a part of
-- another product.
--       Copyright (C) 1994-2002 LORIA - INRIA - U.H.P. Nancy 1 - FRANCE
--          Dominique COLNET and Suzanne COLLIN - SmartEiffel@loria.fr
--                       http://SmartEiffel.loria.fr
--
expanded class BASIC_DIRECTORY
--
-- Very low-level basic tools for file-system directory handling and file
-- path manipulation. This class is intended to be platform independant as
-- much as possible. In order to remove from the client side the burden of
-- file path computation, this class tries to compute automatically the
-- system file notation using argument(s) of some of the very first call(s).
-- As soon as the system notation has been properly detected, the result is
-- internally memorized for all objects of type BASIC_DIRECTORY in a common
-- private buffer. Besides the low-level nature of operations one can found
-- in this class, all file path manipulations are done in a smart way
-- (except when the system file path notation has not been detected
-- automatically, which is quite uncommon). As an example, even if the
-- directory separator is internally detected, this information is
-- _intentionaly_ kept private to avoid low-level manipulation from the
-- client side. Finally, this class is expanded in order to avoid as much as
-- possible memory allocations.
--
-- Also consider high level facade class DIRECTORY if you don't want
-- to deal directly with low level directory streams.
--

feature {NONE}

   directory_stream: POINTER
         -- This pointer memorize the current directory stream being
         -- scanned (used to compute `is_connected').

   current_entry: POINTER
         -- When `is_connected', memorize the current entry in the
         -- current  `directory_stream'.

feature -- State of `Current' basic directory stream:

   is_connected: BOOLEAN is
         -- Is `Current' connected to some directory stream ?
      do
         Result := directory_stream.is_not_null
      end

   end_of_input: BOOLEAN is
         -- Is end of input reached ?
      require
         is_connected
      do
         Result := current_entry.is_null
      end

feature -- Connect and disconnect:

   connect_to(directory_path: STRING) is
         -- Try to connect `Current' to some existing `directory_path'. After
         -- this call, the client is supposed to use `is_connected' to check
         -- that the stream is ready to be used.
      require
         not is_connected
         not directory_path.is_empty
         common_buffer_protection: last_entry /= directory_path
      local
         path_pointer: POINTER
      do
         path_pointer := directory_path.to_external
         directory_stream := basic_directory_open(path_pointer)
         current_entry := directory_stream
         last_entry.clear
      ensure
         is_connected implies not end_of_input
      end

   connect_with(some_path: STRING) is
         -- Try to connect `Current' to some directory using `some_path' which
         -- may  be either an existing directory path or some arbitrary
         -- file path name. When `some_path' is the path of some readable
         -- existing directory, this directory is opened and the effect of
         -- `connect_with' is equivalent to `connect_to'. When `some_path' is not an
         -- existing readable directory path, `connect_with' tries to open the
         -- directory which may contains `some_path' viewed as a file path
         -- name. After this call, the client is supposed to use `is_connected'
         -- to check that the stream is ready to be used and the `last_entry'
         -- buffer to know about the corresponding opened directory path.
         -- Whatever the result, `some_path' is left unchanged.
      require
         not is_connected
         not some_path.is_empty
         common_buffer_protection: last_entry /= some_path
      local
         p: POINTER
      do
         connect_to(some_path)
         if is_connected then
            last_entry.copy(some_path)
         else
            compute_parent_directory_of(some_path)
            if last_entry.count > 0 then
               p := last_entry.to_external
               directory_stream := basic_directory_open(p)
               current_entry := directory_stream
               if directory_stream.is_null then
                  last_entry.clear
               end
            else
               last_entry.clear
            end
         end
      ensure
         is_connected implies not end_of_input
      end

   connect_to_current_working_directory is
         -- Try to connect `Current' to the current working directory.
         -- After this call, the client is supposed to use `is_connected'
         -- to check that the stream is ready to be used and the `last_entry'
         -- buffer to know about the name of the current working directory.
      require
         not is_connected
      local
         path: POINTER
      do
         path := basic_directory_current_working_directory
         if path.is_not_null then
            last_entry.from_external_copy(path)
            directory_stream := basic_directory_open(path)
            current_entry := directory_stream
            if directory_stream.is_null then
               last_entry.clear
            end
         else
            last_entry.clear
         end
      ensure
         is_connected implies not end_of_input
      end

   disconnect is
         -- Do not forget to call this feature when you have finished
         -- with some previously opened directory stream.
      require
         is_connected
      local
         null: POINTER
      do
         if basic_directory_close(directory_stream) then
            directory_stream := null
            current_entry := null
         end
      ensure
         not is_connected
      end

feature -- Scanning:

   last_entry: STRING is
         -- Unique global buffer (once object) to get the last information
         -- computed by many routines of this class: `read_entry', `connect_with'
         -- `connect_to_current_working_directory', `compute_parent_directory_of', ...
      once
         create Result.make(256)
      end

   read_entry is
         -- Read the next entry name and update `last_entry' and `end_of_input'
         -- accordingly.
      require
         is_connected
         not end_of_input
      local
         name: POINTER
      do
         current_entry := basic_directory_read_entry(directory_stream)
         if current_entry.is_not_null then
            name := basic_directory_get_entry_name(current_entry)
            last_entry.from_external_copy(name)
         end
      end

feature -- File path handling tools:

   compute_parent_directory_of(some_path: STRING) is
         -- Using `some_path' (which may be either a file path or a directory
         -- path) tries to compute in the `last_entry' buffer the parent
         -- directory of `some_path'. When `some_path' is a path with no parent
         -- directory, the `last_entry' buffer `is_empty' after this call. This
         -- operation does not perform any disk access.
      require
         not some_path.is_empty
         common_buffer_protection: last_entry /= some_path
      do
         last_entry.copy(some_path)
         if unix_notation then
            from
	       last_entry.remove_last(1)
            until
               last_entry.is_empty or else last_entry.last = '/'
            loop
               last_entry.remove_last(1)
            end
	 elseif windows_notation then
            from
	       last_entry.remove_last(1)
            until
               last_entry.is_empty or else last_entry.last = '\'
            loop
               last_entry.remove_last(1)
            end
	 elseif cygwin_notation then
	    from
	       last_entry.remove_last(1)
            until
               last_entry.is_empty or else
	       last_entry.last = '/' or else
	       last_entry.last = ':'
            loop
               last_entry.remove_last(1)
            end
	    if last_entry.is_empty then
	    elseif last_entry.first = '/' then
	       if last_entry.count = 2 then
		  if last_entry.item(2) = '/' then
		     last_entry.remove_last(1)
		  end
	       end
	    end
	 elseif amiga_notation then
	    if last_entry.last = ':' then
	       last_entry.clear
	    else
	       from
		  last_entry.remove_last(1)
	       until
		  last_entry.is_empty or else
		  last_entry.last = '/' or else
		  last_entry.last = ':'
	       loop
		  last_entry.remove_last(1)
	       end
	    end
         elseif macintosh_notation then
            from
	       last_entry.remove_last(1)
            until
               last_entry.is_empty or else last_entry.last = ':'
            loop
               last_entry.remove_last(1)
            end
         elseif openvms_notation then
            if last_entry.last = ']' then
               from
                  last_entry.remove_last(1)
               until
                  last_entry.is_empty or else
                  last_entry.last = '.' or else
                  last_entry.last = '['
               loop
                  last_entry.remove_last(1)
               end
               if last_entry.count > 0 then
                  inspect
                     last_entry.last
                  when '.' then
                     last_entry.remove_last(1)
                     last_entry.extend(']')
                  when '[' then
                     if last_entry.count = some_path.count - 1 then
                        last_entry.remove_last(1)
		     else
                        last_entry.extend(']')
                     end
                  end
               end
            elseif last_entry.last = ':' then
	       last_entry.clear
            else
               from
                  last_entry.remove_last(1)
               until
                  last_entry.is_empty or else
                  last_entry.last = ']'
               loop
                  last_entry.remove_last(1)
               end
               if last_entry.is_empty then
		  last_entry.clear
               end
            end
         elseif system_notation_detected then
            last_entry.clear
         else
            set_notation_using(some_path)
            if system_notation_detected then
               compute_parent_directory_of(some_path)
            else
               last_entry.clear
            end
         end
      end

   compute_subdirectory_with(parent_path, entry_name: STRING) is
         -- Try to compute in the `last_entry' buffer the new subdirectory
         -- path obtained when trying to concatenate smartly `parent_path'
         -- whith some `entry_name'. When this fails the `last_entry' buffer `is_empty'
         -- after this call. This operation does not perform any disk access.
         -- Whatever the result, `parent_path' and  `entry_name' are left unchanged.
      require
         not parent_path.is_empty
         not entry_name.is_empty
         common_buffer_protection1: last_entry /= parent_path
         common_buffer_protection2: last_entry /= entry_name
      do
         last_entry.copy(parent_path)
         if unix_notation or else cygwin_notation then
            if (once ".").is_equal(entry_name) then
               -- Because you would get the same directory as `parent_path' and
               -- not a new subdirectory as explained before.
               last_entry.clear
            elseif (once "..").is_equal(entry_name) then
               -- Because you would not get a subdirectory of `parent_path'.
               last_entry.clear
            else
               last_entry.extend_unless('/')
               if entry_name.first = '/' then
                  last_entry.remove_last(1)
               end
               last_entry.append(entry_name)
               last_entry.extend_unless('/')
            end
         elseif windows_notation then
            if (once ".").is_equal(entry_name) then
               -- Because you would get the same directory as `parent_path' and
               -- not a new subdirectory as explained before.
               last_entry.clear
            elseif (once "..").is_equal(entry_name) then
               -- Because you would not get a subdirectory of `parent_path'.
               last_entry.clear
            else
               last_entry.extend_unless('\')
               if entry_name.first = '\' then
                  last_entry.remove_last(1)
               end
               last_entry.append(entry_name)
               last_entry.extend_unless('\')
            end
         elseif amiga_notation then
            inspect
               last_entry.last
            when '/' then
               if entry_name.first = '/' then
                  last_entry.remove_last(1)
               end
            when ':' then
            else
               if entry_name.first /= '/' then
                  last_entry.add_last('/')
               end
            end
            last_entry.append(entry_name)
            last_entry.extend_unless('/')
         elseif macintosh_notation then
            last_entry.extend_unless(':')
            if entry_name.first = ':' then
               last_entry.remove_last(1)
            end
            last_entry.append(entry_name)
            last_entry.extend_unless(':')
         elseif openvms_notation then
	    if last_entry.count = 1 then
	       last_entry.clear
            elseif last_entry.last = ']' then
	       last_entry.remove_last(1)
	       if last_entry.last = '[' then
		  last_entry.append(entry_name)
		  last_entry.extend(']')
	       else
		  last_entry.extend('.')
		  last_entry.append(entry_name)
		  last_entry.extend(']')
	       end
	    elseif last_entry.last = ':' then
	       last_entry.extend('[')
	       last_entry.append(entry_name)
	       last_entry.extend(']')
	    else
	       last_entry.clear
            end
         elseif system_notation_detected then
            last_entry.clear
         else
            set_notation_using(parent_path)
            if system_notation_detected then
               compute_subdirectory_with(parent_path,entry_name)
            else
               last_entry.clear
            end
         end
      end

   compute_file_path_with(parent_path, file_name: STRING) is
         -- Try to compute in the `last_entry' buffer the new file path obtained
         -- when trying to concatenate smartly `parent_path' whith some
         -- `file_name'. When this fails the `last_entry' buffer `is_empty' after
         -- this call. This operation does not perform any disk access.
         -- Whatever the result, `parent_path' and `file_name' are left unchanged.
      require
         not parent_path.is_empty
         not file_name.is_empty
         common_buffer_protection1: last_entry /= parent_path
         common_buffer_protection2: last_entry /= file_name
      do
         last_entry.copy(parent_path)
         if unix_notation or else cygwin_notation then
            last_entry.extend_unless('/')
            if file_name.first = '/' then
               last_entry.remove_last(1)
            end
            last_entry.append(file_name)
         elseif windows_notation then
            last_entry.extend_unless('\')
            if file_name.first = '\' then
               last_entry.remove_last(1)
            end
            last_entry.append(file_name)
         elseif amiga_notation then
            inspect
               last_entry.last
            when ':' then
            when '/' then
               if file_name.first = '/' then
                  last_entry.remove_last(1)
               end
            else
               if file_name.first /= '/' then
                  last_entry.extend('/')
               end
            end
            last_entry.append(file_name)
         elseif macintosh_notation then
            last_entry.extend_unless(':')
            if file_name.first = ':' then
               last_entry.remove_last(1)
            end
            last_entry.append(file_name)
         elseif openvms_notation then
            inspect
               last_entry.last
            when ']' then
            when '.' then
               last_entry.remove_last(1)
               last_entry.extend(']')
            else
               last_entry.extend(']')
            end
            if file_name.first = ']' then
               last_entry.remove_last(1)
            end
            last_entry.append(file_name)
         elseif system_notation_detected then
            last_entry.clear
         else
            set_notation_using(parent_path)
            if system_notation_detected then
               compute_file_path_with(parent_path,file_name)
            else
               last_entry.clear
            end
         end
      end

   change_current_working_directory(directory_path: STRING) is
         -- Try to change the current working directory using some
         -- `directory_path'. When the operation is possible, the `last_entry' buffer
         -- is updated with the new current working directory path,
         -- otherwise, when the modification is not possible the `last_entry'
         -- buffer `is_empty' after this call. Whatever the result,
         -- `directory_path' is left unchanged.
      require
         not is_connected
         common_buffer_protection1: last_entry /= directory_path
      local
         p: POINTER
      do
         p := directory_path.to_external
         if basic_directory_chdir(p) then
            connect_to_current_working_directory
            if is_connected then
               disconnect
               check not last_entry.is_empty end
            else
               last_entry.clear
            end
         else
            last_entry.clear
         end
      ensure
         not is_connected
      end

feature -- Disk modification:

   create_new_directory(directory_path: STRING): BOOLEAN is
         -- Try to create a new directory using the `directory_path' name.
         -- Returns true on success.
      require
         not is_connected
      local
         p: POINTER
      do
         p := directory_path.to_external
         Result := basic_directory_mkdir(p)
      ensure
         not is_connected
      end

   remove_directory(directory_path: STRING): BOOLEAN is
         -- Try to remove directory `directory_path' which must be empty.
         -- Returns true on success.
      require
         not is_connected
      local
         p: POINTER
      do
         p := directory_path.to_external
         Result := basic_directory_rmdir(p)
      ensure
         not is_connected
      end

   remove_files_of(directory_path: STRING) is
         -- Try to remove all files (not subdirectories) of directory
         -- specified by `directory_path'.
      require
         not is_connected
      do
         connect_to(directory_path)
         if is_connected then
            from
               read_entry
            until
               end_of_input
            loop
               tmp_path.copy(last_entry)
               compute_file_path_with(directory_path,tmp_path)
               tmp_path.copy(last_entry)
               remove_file(tmp_path)
               read_entry
            end
            disconnect
         end
      ensure
         not is_connected
      end

feature -- Miscellaneous:

   is_case_sensitive: BOOLEAN is
      local
	 bd: like Current
      do
	 inspect
	    notation.first
	 when '?' then
	    bd.connect_to_current_working_directory
	    if bd.is_connected then
	       if not last_entry.is_empty then
		  set_notation_using(last_entry)
	       end
	       bd.disconnect
	       if notation.first /= '?' then
		  Result := is_case_sensitive
	       end
	    end
	 when 'W', 'V' then
	 else
	    Result := true
	 end
      end

feature {INSTALL,SYSTEM_TOOLS}

   notation: STRING is "?"
         -- Unique common buffer to memorize the system path
	 -- name notation code.

   valid_notation: BOOLEAN is
      do
	 Result := ((notation.count = 1)
		    and then
		    (once "?UWCVAM").has(notation.first))
      end

   system_notation_detected: BOOLEAN is
      require
	 valid_notation
      do
	 check valid_notation end
         Result := notation.first /= '?'
      end

   unix_notation: BOOLEAN is
         -- The Windows like file path notation looks like:
         --   /SmartEiffel/sys/system.se
      require
	 valid_notation
      do
         Result := notation.first = 'U'
      end

   windows_notation: BOOLEAN is
         -- The Windows like file path notation looks like:
         --   C:\SmartEiffel\sys\system.se
      require
	 valid_notation
      do
         Result := notation.first = 'W'
      end

   cygwin_notation: BOOLEAN is
         -- The Cygwin like file path notation looks like:
         --   //C/SmartEiffel/sys/system.se
      require
	 valid_notation
      do
         Result := notation.first = 'C'
      end

   amiga_notation: BOOLEAN is
         -- The Amiga file path notation looks like:
         --   DEV:SmartEiffel/sys/system.se
      require
	 valid_notation
      do
         Result := notation.first = 'A'
      end

   macintosh_notation: BOOLEAN is
         -- The Macintosh file path notation looks like:
         --   :SmartEiffel:sys:system.se
      require
	 valid_notation
      do
         Result := notation.first = 'M'
      end

   openvms_notation: BOOLEAN is
         -- The VMS file path notation looks like:
         --    DISK:[SmartEiffel.sys]system.se
         -- The current working directory notation is:
         --    DISK:[]
         -- The equivalent of Unix .. is :
         --    [-]
         -- The equivalent of Unix ../.. is :
         --    [-.-]
         --
      require
	 valid_notation
      do
         Result := notation.first = 'V'
      end

   set_notation_using(some_path: STRING) is
	 -- Try to detect automatically the file system notation.
      require
	 not some_path.is_empty
         not system_notation_detected
      do
	 inspect
	    some_path.first
	 when '/', '.', '~' then
	    notation.put('U',1)
	    if some_path.count >= 4 then
	       if some_path.item(2) = '/' then
		  if some_path.item(4) = '/' then
		     notation.put('C',1)
		  end
	       end
	    end
	 when '\' then
	    notation.put('W',1)
	 when ':' then
	    notation.put('M',1)
	 when '[' then
	    notation.put('V',1)
	 when 'a'..'z', 'A'..'Z' then
	    if some_path.count >= 2 then
	       inspect
		  some_path.item(2)
	       when ':' then
		  if some_path.count = 2 then
		     notation.put('W',1)
		  elseif some_path.has('\') then
		     notation.put('W',1)
		  elseif some_path.has('?') then
		     notation.put('A',1)
		  elseif some_path.has('/') then
		     notation.put('C',1)
		  end
	       when 'a'..'z','A'..'Z' then
		  if some_path.has('[') then
		     notation.put('V',1)
		  elseif some_path.has(':') then
		     if some_path.has('/') then
			notation.put('A',1)
		     elseif some_path.has('[') then
			notation.put('V',1)
		     else
			notation.put('A',1)
		     end
		  elseif some_path.has('/') then
		     notation.put('U',1)
		  elseif some_path.has('\') then
		     notation.put('U',1)
		  end
	       else
	       end
	    end
	 else
	 end
      end

   reset_notation_using(some_path: STRING) is
	 -- Try to detect automatically the file system notation.
      do
	 notation.put('?',1)
	 set_notation_using(some_path)
      end

   tmp_path: STRING is
      once
         create Result.make(256)
      end

feature {NONE}

   basic_directory_open(path_pointer: POINTER): POINTER is
         -- Try to open some existing directory using `path'. When `Result'
         -- `is_not_null', the directory is correctly opened and `Result' is
         -- a valid handle for this directory. Using `Result', one can
         -- then scan the content of the directory using function
         -- `basic_directory_read_entry' and `basic_directory_get_entry_name'. Finally,
         -- a `is_not_null' directory must be closed using function
         -- `basic_directory_close'.
      require
         path_pointer.is_not_null
      external "SmartEiffel"
      end

   basic_directory_read_entry(dirstream: POINTER): POINTER is
         -- Read an return a new entry using the directory handle `dirstream'
         -- obtained with function `basic_directory_open'. When there is no more
         -- entry, the `Result' becomes `is_null'.
      require
         dirstream.is_not_null
      external "SmartEiffel"
      end

   basic_directory_get_entry_name(entry: POINTER): POINTER is
         -- Read an return a new entry using the directory handle `dirstream'
         -- obtained with function `basic_directory_open'.
         -- When there is no more entry, the `Result' becomes `is_null'.
      require
         entry.is_not_null
      external "SmartEiffel"
      end

   basic_directory_close(dirstream: POINTER): BOOLEAN is
         -- Try to close some opened `dirstream' directory.
         -- A true result indicates that the directory is correctly
         -- closed.
      require
         dirstream.is_not_null
      external "SmartEiffel"
      end

   basic_directory_current_working_directory: POINTER is
         -- Try to get the current working directory path.
      external "SmartEiffel"
      end

   basic_directory_chdir(destination: POINTER): BOOLEAN is
         -- Try to change the current working directory using `destination'.
      external "SmartEiffel"
      end

   basic_directory_mkdir(directory_path: POINTER): BOOLEAN is
         -- Try to create a new directory using `directory_path'.
      external "SmartEiffel"
      end

   basic_directory_rmdir(directory_path: POINTER): BOOLEAN is
         -- Try to remove `directory_path'.
      external "SmartEiffel"
      end

end -- BASIC_DIRECTORY
