require 'ruby_smb/server/share/provider/disk'
require 'ruby_smb/server/share/provider/virtual_disk/virtual_file'
require 'ruby_smb/server/share/provider/virtual_disk/virtual_pathname'
require 'ruby_smb/server/share/provider/virtual_disk/virtual_stat'

module RubySMB
  class Server
    module Share
      module Provider
        # This is a share provider that exposes a virtual file system whose entries do not exist on disk.
        # @since 3.1.1
        class VirtualDisk < Disk
          HASH_METHODS = %i[ [] each each_key each_value keys length values ]
          private_constant :HASH_METHODS

          # @param [String] name The name of this share.
          def initialize(name, hooks: nil)
            @vfs = {}
            super(name, add(VirtualPathname.new(self, File::SEPARATOR)), hooks: hooks)
          end

          # Add a dynamic file to the virtual file system. A dynamic file is one whose contents are generated by the
          # specified block. The contents are generated when the share is opened.
          #
          # @param [String] path The path of the file to add, relative to the share root.
          # @param [File::Stat] stat An explicit stat object describing the file.
          # @yield The content generation routine.
          # @yieldreturn [String] The generated file content.
          def add_dynamic_file(path, stat: nil, &block)
            raise ArgumentError.new('a block must be specified for dynamic files') unless block_given?
            path = VirtualPathname.cleanpath(path)
            path = File::SEPARATOR + path unless path.start_with?(File::SEPARATOR)
            raise ArgumentError.new('must be a file') if stat && !stat.file?

            vf = VirtualDynamicFile.new(self, path, stat: stat, &block)
            add(vf)
          end

          # Add a mapped file to the virtual file system. A mapped file is one who is backed by an entry on
          # disk. The path need not be present, but if it does exist, it must be a file.
          #
          # @param [String] path The path of the file to add, relative to the share root.
          # @param [String, Pathname] mapped_path The path on the local file system to map into the virtual file system.
          def add_mapped_file(path, mapped_path)
            path = VirtualPathname.cleanpath(path)
            path = File::SEPARATOR + path unless path.start_with?(File::SEPARATOR)

            vf = VirtualMappedFile.new(self, path, mapped_path)
            add(vf)
          end

          # Add a static file to the virtual file system. A static file is one whose contents are known at creation time
          # and do not change. If *content* is a file-like object that responds to #read, the data will be read using
          # that method. Likewise, if *content* has a #stat attribute and *stat* was not specified, then the value of
          # content's #stat attribute will be used.
          #
          # @param [String] path The path of the file to add, relative to the share root.
          # @param [String, #read, #stat] content The static content to add.
          # @param [File::Stat] stat An explicit stat object describing the file.
          def add_static_file(path, content, stat: nil)
            path = VirtualPathname.cleanpath(path)
            path = File::SEPARATOR + path unless path.start_with?(File::SEPARATOR)
            raise ArgumentError.new('must be a file') if stat && !stat.file?

            stat = content.stat if content.respond_to?(:stat) && stat.nil?
            content = content.read if content.respond_to?(:read)
            vf = VirtualStaticFile.new(self, path, content, stat: stat)
            add(vf)
          end

          def add(virtual_pathname)
            raise ArgumentError.new('paths must be absolute') unless virtual_pathname.absolute?

            path = virtual_pathname.to_s
            raise ArgumentError.new('paths must be normalized') unless VirtualPathname.cleanpath(path) == path

            path_parts = path.split(VirtualPathname::SEPARATOR)
            2.upto(path_parts.length - 1) do |idx|
              ancestor = path_parts[0...idx].join(path[VirtualPathname::SEPARATOR])
              next if @vfs[ancestor]&.directory?

              @vfs[ancestor] = VirtualPathname.new(self, ancestor, stat: VirtualStat.new(directory?: true))
            end

            @vfs[path] = virtual_pathname
          end

          def new_processor(server_client, session)
            scoped_virtual_disk = self.class.new(@name, hooks: @hooks)
            @vfs.each_value do |path|
              path = path.dup
              path.virtual_disk = scoped_virtual_disk if path.is_a?(VirtualPathname)
              path = path.generate(server_client, session) if path.is_a?(VirtualDynamicFile)
              scoped_virtual_disk.add(path)
            end
            self.class::Processor.new(scoped_virtual_disk, server_client, session)
          end

          private

          def method_missing(symbol, *args)
            if HASH_METHODS.include?(symbol)
              return @vfs.send(symbol, *args)
            end

            raise NoMethodError, "undefined method `#{symbol}' for #{self.class}"
          end

          def respond_to_missing?(symbol, include_private = false)
            HASH_METHODS.include?(symbol)
          end
        end
      end
    end
  end
end
