package storage

import (
	"path/filepath"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/storage/repository"
)

// NewFileURI creates a new URI from the given file path.
// Relative paths will be converted to absolute using filepath.Abs if required.
func NewFileURI(path string) fyne.URI {
	assumeAbs := false // avoid filepath.IsAbs as it follows platform rules
	if len(path) >= 1 {
		if path[0] == '/' {
			assumeAbs = true
		} else if len(path) >= 2 {
			assumeAbs = path[1] == ':'
		}
	}

	if !assumeAbs {
		absolute, err := filepath.Abs(path)
		if err == nil {
			path = absolute
		}
	}

	return repository.NewFileURI(path)
}

// NewURI creates a new URI from the given string representation. This could be
// a URI from an external source or one saved from URI.String()
//
// Deprecated: use ParseURI instead
func NewURI(s string) fyne.URI {
	u, _ := ParseURI(s)
	return u
}

// ParseURI creates a new URI instance by parsing a URI string.
//
// Parse URI will parse up to the first ':' present in the URI string to
// extract the scheme, and then delegate further parsing to the registered
// repository for the given scheme. If no repository is registered for that
// scheme, the URI is parsed on a best-effort basis using net/url.
//
// As a special exception, URIs beginning with 'file:' are always parsed using
// NewFileURI(), which will correctly handle back-slashes appearing in the URI
// path component on Windows.
//
// Since: 2.0
func ParseURI(s string) (fyne.URI, error) {
	return repository.ParseURI(s)
}

// Parent returns a URI referencing the parent resource of the resource
// referenced by the URI. For example, the Parent() of 'file://foo/bar.baz' is
// 'file://foo'. The URI which is returned will be listable.
//
// NOTE: it is not a given that Parent() return a parent URI with the same
// Scheme(), though this will normally be the case.
//
// This can fail in several ways:
//
//   - If the URI refers to a filesystem root, then the Parent() implementation
//     must return (nil, URIRootError).
//
//   - If the URI refers to a resource which does not exist in a hierarchical
//     context (e.g. the URI references something which does not have a
//     semantically meaningful "parent"), the Parent() implementation may return
//     an error.
//
//   - If determining the parent of the referenced resource requires
//     interfacing with some external system, failures may propagate
//     through the Parent() implementation. For example if determining
//     the parent of a file:// URI requires reading information from
//     the filesystem, it could fail with a permission error.
//
//   - If the scheme of the given URI does not have a registered
//     HierarchicalRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// NOTE: since v2.0.0, Parent() is backed by the repository system - this
// function is a helper which calls into an appropriate repository instance for
// the scheme of the URI it is given.
//
// Since: 1.4
func Parent(u fyne.URI) (fyne.URI, error) {

	repo, err := repository.ForURI(u)
	if err != nil {
		return nil, err
	}

	hrepo, ok := repo.(repository.HierarchicalRepository)
	if !ok {
		return nil, repository.ErrOperationNotSupported
	}

	return hrepo.Parent(u)
}

// Child returns a URI referencing a resource nested hierarchically below the
// given URI, identified by a string. For example, the child with the string
// component 'quux' of 'file://foo/bar' is 'file://foo/bar/quux'.
//
// This can fail in several ways:
//
//   - If the URI refers to a resource which does not exist in a hierarchical
//     context (e.g. the URI references something which does not have a
//     semantically meaningful "child"), the Child() implementation may return an
//     error.
//
//   - If generating a reference to a child of the referenced resource requires
//     interfacing with some external system, failures may propagate through the
//     Child() implementation. It is expected that this case would occur very
//     rarely if ever.
//
//   - If the scheme of the given URI does not have a registered
//     HierarchicalRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// NOTE: since v2.0.0, Child() is backed by the repository system - this
// function is a helper which calls into an appropriate repository instance for
// the scheme of the URI it is given.
//
// Since: 1.4
func Child(u fyne.URI, component string) (fyne.URI, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return nil, err
	}

	hrepo, ok := repo.(repository.HierarchicalRepository)
	if !ok {
		return nil, repository.ErrOperationNotSupported
	}

	return hrepo.Child(u, component)
}

// Exists determines if the resource referenced by the URI exists.
//
// This can fail in several ways:
//
//   - If checking the existence of a resource requires interfacing with some
//     external system, then failures may propagate through Exists(). For
//     example, checking the existence of a resource requires reading a directory
//     may result in a permissions error.
//
// It is understood that a non-nil error value signals that the existence or
// non-existence of the resource cannot be determined and is undefined.
//
// NOTE: since v2.0.0, Exists is backed by the repository system - this function
// calls into a scheme-specific implementation from a registered repository.
//
// Exists may call into either a generic implementation, or into a
// scheme-specific implementation depending on which storage repositories have
// been registered.
//
// Since: 1.4
func Exists(u fyne.URI) (bool, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return false, err
	}

	return repo.Exists(u)
}

// Delete destroys, deletes, or otherwise removes the resource referenced
// by the URI.
//
// This can fail in several ways:
//
//   - If removing the resource requires interfacing with some external system,
//     failures may propagate through Destroy(). For example, deleting a file may
//     fail with a permissions error.
//
//   - If the referenced resource does not exist, attempting to destroy it should
//     throw an error.
//
//   - If the scheme of the given URI does not have a registered
//     WritableRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// Delete is backed by the repository system - this function calls
// into a scheme-specific implementation from a registered repository.
//
// Since: 2.0
func Delete(u fyne.URI) error {
	repo, err := repository.ForURI(u)
	if err != nil {
		return err
	}

	wrepo, ok := repo.(repository.WritableRepository)
	if !ok {
		return repository.ErrOperationNotSupported
	}

	return wrepo.Delete(u)

}

// Reader returns URIReadCloser set up to read from the resource that the
// URI references.
//
// This method can fail in several ways:
//
//   - Different permissions or credentials are required to read the
//     referenced resource.
//
//   - This URI scheme could represent some resources that can be read,
//     but this particular URI references a resources that is not
//     something that can be read.
//
//   - Attempting to set up the reader depended on a lower level
//     operation such as a network or filesystem access that has failed
//     in some way.
//
// Reader is backed by the repository system - this function calls
// into a scheme-specific implementation from a registered repository.
//
// Since: 2.0
func Reader(u fyne.URI) (fyne.URIReadCloser, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return nil, err
	}

	return repo.Reader(u)
}

// CanRead determines if a given URI could be written to using the Reader()
// method. It is preferred to check if a URI is readable using this method
// before calling Reader(), because the underlying operations required to
// attempt to read and then report an error may be slower than the operations
// needed to test if a URI is readable. Keep in mind however that even if
// CanRead returns true, you must still do appropriate error handling for
// Reader(), as the underlying filesystem may have changed since you called
// CanRead.
//
// The non-existence of a resource should not be treated as an error. In other
// words, a Repository implementation which for some URI u returns false, nil
// for Exists(u), CanRead(u) should also return false, nil.
//
// CanRead is backed by the repository system - this function calls into a
// scheme-specific implementation from a registered repository.
//
// Since: 2.0
func CanRead(u fyne.URI) (bool, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return false, err
	}

	return repo.CanRead(u)
}

// Writer returns URIWriteCloser set up to write to the resource that the
// URI references.
//
// Writing to a non-extant resource should create that resource if possible
// (and if not possible, this should be reflected in the return of CanWrite()).
// Writing to an extant resource should overwrite it in-place. At present, this
// API does not provide a mechanism for appending to an already-extant
// resource, except for reading it in and writing all the data back out.
//
// This method can fail in several ways:
//
//   - Different permissions or credentials are required to write to the
//     referenced resource.
//
//   - This URI scheme could represent some resources that can be
//     written, but this particular URI references a resources that is
//     not something that can be written.
//
//   - Attempting to set up the writer depended on a lower level
//     operation such as a network or filesystem access that has failed
//     in some way.
//
//   - If the scheme of the given URI does not have a registered
//     WritableRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// Writer is backed by the repository system - this function calls into a
// scheme-specific implementation from a registered repository.
//
// Since: 2.0
func Writer(u fyne.URI) (fyne.URIWriteCloser, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return nil, err
	}

	wrepo, ok := repo.(repository.WritableRepository)
	if !ok {
		return nil, repository.ErrOperationNotSupported
	}

	return wrepo.Writer(u)
}

// CanWrite determines if a given URI could be written to using the Writer()
// method. It is preferred to check if a URI is writable using this method
// before calling Writer(), because the underlying operations required to
// attempt to write and then report an error may be slower than the operations
// needed to test if a URI is writable.  Keep in mind however that even if
// CanWrite returns true, you must still do appropriate error handling for
// Writer(), as the underlying filesystem may have changed since you called
// CanWrite.

// CanWrite is backed by the repository system - this function calls into a
// scheme-specific implementation from a registered repository.
//
// Since: 2.0
func CanWrite(u fyne.URI) (bool, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return false, err
	}

	wrepo, ok := repo.(repository.WritableRepository)
	if !ok {
		return false, repository.ErrOperationNotSupported
	}

	return wrepo.CanWrite(u)
}

// Copy given two URIs, 'src', and 'dest' both of the same scheme, will copy
// one to the other. If the source and destination are of different schemes,
// then the Copy implementation for the storage repository registered to the
// scheme of the source will be used. Implementations are recommended to use
// repository.GenericCopy() as a fail-over in the case that they do not
// understand how to operate on the scheme of the destination URI. However, the
// behavior of calling Copy() on URIs of non-matching schemes is ultimately
// defined by the storage repository registered to the scheme of the source
// URI.
//
// This method may fail in several ways:
//
//   - Different permissions or credentials are required to perform the
//     copy operation.
//
//   - This URI scheme could represent some resources that can be copied,
//     but either the source, destination, or both are not resources
//     that support copying.
//
//   - Performing the copy operation depended on a lower level operation
//     such as network or filesystem access that has failed in some way.
//
//   - If the scheme of the given URI does not have a registered
//     CopyableRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// Copy is backed by the repository system - this function calls into a
// scheme-specific implementation from a registered repository.
//
// Since: 2.0
func Copy(source fyne.URI, destination fyne.URI) error {
	repo, err := repository.ForURI(source)
	if err != nil {
		return err
	}

	crepo, ok := repo.(repository.CopyableRepository)
	if !ok {
		return repository.ErrOperationNotSupported
	}

	return crepo.Copy(source, destination)
}

// Move returns a method that given two URIs, 'src' and 'dest' both of the same
// scheme this will move src to dest. This means the resource referenced by
// src will be copied into the resource referenced by dest, and the resource
// referenced by src will no longer exist after the operation is complete.
//
// If the source and destination are of different schemes, then the Move
// implementation for the storage repository registered to the scheme of the
// source will be used. Implementations are recommended to use
// repository.GenericMove() as a fail-over in the case that they do not
// understand how to operate on the scheme of the destination URI. However, the
// behavior of calling Move() on URIs of non-matching schemes is ultimately
// defined by the storage repository registered to the scheme of the source
// URI.
//
// This method may fail in several ways:
//
//   - Different permissions or credentials are required to perform the
//     rename operation.
//
//   - This URI scheme could represent some resources that can be renamed,
//     but either the source, destination, or both are not resources
//     that support renaming.
//
//   - Performing the rename operation depended on a lower level operation
//     such as network or filesystem access that has failed in some way.
//
//   - If the scheme of the given URI does not have a registered
//     MovableRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// Move is backed by the repository system - this function calls into a
// scheme-specific implementation from a registered repository.
//
// Since: 2.0
func Move(source fyne.URI, destination fyne.URI) error {
	repo, err := repository.ForURI(source)
	if err != nil {
		return err
	}

	mrepo, ok := repo.(repository.MovableRepository)
	if !ok {
		return repository.ErrOperationNotSupported
	}

	return mrepo.Move(source, destination)
}

// CanList will determine if the URI is listable or not.
//
// This method may fail in several ways:
//
//   - Different permissions or credentials are required to check if the
//     URI supports listing.
//
//   - This URI scheme could represent some resources that can be listed,
//     but this specific URI is not one of them (e.g. a file on a
//     filesystem, as opposed to a directory).
//
//   - Checking for listability depended on a lower level operation
//     such as network or filesystem access that has failed in some way.
//
//   - If the scheme of the given URI does not have a registered
//     ListableRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// CanList is backed by the repository system - this function calls into a
// scheme-specific implementation from a registered repository.
//
// Since: 2.0
func CanList(u fyne.URI) (bool, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return false, err
	}

	lrepo, ok := repo.(repository.ListableRepository)
	if !ok {
		return false, repository.ErrOperationNotSupported
	}

	return lrepo.CanList(u)
}

// List returns a list of URIs that reference resources which are nested below
// the resource referenced by the argument. For example, listing a directory on
// a filesystem should return a list of files and directories it contains.
//
// This method may fail in several ways:
//
//   - Different permissions or credentials are required to obtain a
//     listing for the given URI.
//
//   - This URI scheme could represent some resources that can be listed,
//     but this specific URI is not one of them (e.g. a file on a
//     filesystem, as opposed to a directory). This can be tested in advance
//     using the Listable() function.
//
//   - Obtaining the listing depended on a lower level operation such as
//     network or filesystem access that has failed in some way.
//
//   - If the scheme of the given URI does not have a registered
//     ListableRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// List is backed by the repository system - this function either calls into a
// scheme-specific implementation from a registered repository, or fails with a
// URIOperationNotSupported error.
//
// Since: 2.0
func List(u fyne.URI) ([]fyne.URI, error) {
	repo, err := repository.ForURI(u)
	if err != nil {
		return nil, err
	}

	lrepo, ok := repo.(repository.ListableRepository)
	if !ok {
		return nil, repository.ErrOperationNotSupported
	}

	return lrepo.List(u)
}

// CreateListable creates a new listable resource referenced by the given URI.
// CreateListable will error if the URI already references an extant resource.
// This method is used for storage repositories where listable resources are of
// a different underlying type than other resources - for example, in a typical
// filesystem ('file://'), CreateListable() corresponds to directory creation,
// and Writer() implies file creation for non-extant operands.
//
// For storage repositories where listable and non-listable resources are the
// of the same underlying type, CreateListable should be equivalent to calling
// Writer(), writing zero bytes, and then closing the `URIWriteCloser - in
// filesystem terms, the same as calling 'touch;'.
//
// Storage repositories which support listing, but not creation of listable
// objects may return repository.ErrOperationNotSupported.
//
// CreateListable should generally fail if the parent of it's operand does not
// exist, however this can vary by the implementation details of the specific
// storage repository. In filesystem terms, this function is "mkdir" not "mkdir
// -p".
//
// This method may fail in several ways:
//
//   - Different permissions or credentials are required to create the requested
//     resource.
//
//   - Creating the resource depended on a lower level operation such as network
//     or filesystem access that has failed in some way.
//
//   - If the scheme of the given URI does not have a registered
//     ListableRepository instance, then this method will fail with a
//     repository.ErrOperationNotSupported.
//
// CreateListable is backed by the repository system - this function either
// calls into a scheme-specific implementation from a registered repository, or
// fails with a URIOperationNotSupported error.
//
// Since: 2.0
func CreateListable(u fyne.URI) error {
	repo, err := repository.ForURI(u)
	if err != nil {
		return err
	}

	lrepo, ok := repo.(repository.ListableRepository)
	if !ok {
		return repository.ErrOperationNotSupported
	}

	return lrepo.CreateListable(u)
}
