#region License

/*
 * Copyright © 2002-2005 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#endregion

#region Imports

using System;
using System.IO;
using System.Text;
using Spring.Util;

#endregion

namespace Spring.Core.IO
{
    /// <summary>
    /// Convenience base class for <see cref="Spring.Core.IO.IResource"/>
    /// implementations, pre-implementing typical behavior.
    /// </summary>
    /// <remarks>
    /// <p>
    /// The <see cref="Spring.Core.IO.AbstractResource.Exists"/> method will
    /// check whether a <see cref="System.IO.FileInfo"/> or
    /// <see cref="System.IO.Stream"/> can be opened;
    /// <see cref="Spring.Core.IO.AbstractResource.IsOpen"/> will always return
    /// <see langword="false"/>;
    /// <see cref="Spring.Core.IO.AbstractResource.Uri"/> and
    /// <see cref="Spring.Core.IO.AbstractResource.File"/> throw an exception;
    /// and <see cref="Spring.Core.IO.AbstractResource.ToString()"/> will
    /// return the value of the
    /// <see cref="Spring.Core.IO.IResource.Description"/> property.
    /// </p>
    /// </remarks>
    /// <author>Juergen Hoeller</author>
    /// <author>Rick Evans (.NET)</author>
    /// <author>Aleksandar Seovic (.NET)</author>
    /// <seealso cref="Spring.Core.IO.IResource"/>
    public abstract class AbstractResource : IResource
    {
        /// <summary>
        /// The default special character that denotes the base (home, or root)
        /// path.
        /// </summary>
        /// <remarks>
        /// <p>
        /// Will be resolved (by those <see cref="Spring.Core.IO.IResource"/>
        /// implementations that support it) to the home (or root) path for
        /// the specific <see cref="Spring.Core.IO.IResource"/> implementation.
        /// </p>
        /// <p>
        /// For example, in the case of a web application this will (probably)
        /// resolve to the virtual directory of said web application.
        /// </p>
        /// </remarks>
        protected const string DefaultBasePathPlaceHolder = "~";

        private string protocol;
        private string resourceName;
        private string basePathPlaceHolder = DefaultBasePathPlaceHolder;

        #region Constructor (s) / Destructor

        /// <summary>
        /// Creates a new instance of the
        /// <see cref="Spring.Core.IO.AbstractResource"/> class.
        /// </summary>
        /// <remarks>
        /// <p>
        /// This is an <see langword="abstract"/> class, and as such exposes no
        /// public constructors.
        /// </p>
        /// </remarks>
        protected AbstractResource()
        {
        }

        /// <summary>
        /// Creates a new instance of the
        /// <see cref="Spring.Core.IO.AbstractResource"/> class.
        /// </summary>
        /// <remarks>
        /// <p>
        /// This is an <see langword="abstract"/> class, and as such exposes no
        /// public constructors.
        /// </p>
        /// </remarks>
        /// <param name="resourceName">
        /// A string representation of the resource.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// If the supplied <paramref name="resourceName"/> is
        /// <see langword="null"/> or contains only whitespace character(s).
        /// </exception>
        protected AbstractResource(string resourceName)
        {
            AssertUtils.ArgumentHasText(resourceName, "resourceName");
            this.protocol = ConfigurableResourceLoader.GetProtocol(resourceName);
            this.resourceName = resourceName;
        }

        #endregion

        #region Properties

        /// <summary>
        /// The special character that denotes the base (home, or root)
        /// path.
        /// </summary>
        /// <remarks>
        /// <p>
        /// Will be resolved (by those <see cref="Spring.Core.IO.IResource"/>
        /// implementations that support it) to the home (or root) path for
        /// the specific <see cref="Spring.Core.IO.IResource"/> implementation.
        /// </p>
        /// <p>
        /// For example, in the case of a web application this will (probably)
        /// resolve to the virtual directory of said web application.
        /// </p>
        /// </remarks>
        /// <seealso cref="Spring.Core.IO.AbstractResource.DefaultBasePathPlaceHolder"/>
        public string BasePathPlaceHolder
        {
            get { return basePathPlaceHolder; }
            set { basePathPlaceHolder = value; }
        }

        /// <summary>
        /// Return an <see cref="System.IO.Stream"/> for this resource.
        /// </summary>
        /// <value>
        /// An <see cref="System.IO.Stream"/>.
        /// </value>
        /// <exception cref="System.IO.IOException">
        /// If the stream could not be opened.
        /// </exception>
        /// <seealso cref="Spring.Core.IO.IInputStreamSource.InputStream"/>
        public abstract Stream InputStream { get; }

        /// <summary>
        /// Returns a description for this resource.
        /// </summary>
        /// <value>
        /// A description for this resource.
        /// </value>
        /// <seealso cref="Spring.Core.IO.IResource.Description"/>
        public abstract string Description { get; }

        /// <summary>
        /// Returns the protocol associated with this resource (if any).
        /// </summary>
        /// <remarks>
        /// <p>
        /// The value of this property may be <see langword="null"/> if no
        /// protocol is associated with the resource type (for example if the
        /// resource is a memory stream).
        /// </p>
        /// </remarks>
        /// <value>
        /// The protocol associated with this resource (if any).
        /// </value>
        public string Protocol
        {
            get { return protocol; }
        }

        /// <summary>
        /// Does this resource represent a handle with an open stream?
        /// </summary>
        /// <remarks>
        /// <p>
        /// This, the default implementation, always returns
        /// <see langword="false"/>.
        /// </p>
        /// </remarks>
        /// <value>
        /// <see langword="true"/> if this resource represents a handle with an
        /// open stream.
        /// </value>
        /// <seealso cref="Spring.Core.IO.IResource.IsOpen"/>
        public virtual bool IsOpen
        {
            get { return false; }
        }

#if !NET_2_0
        /// <summary>
        /// Returns the <see cref="System.Uri"/> handle for this resource.
        /// </summary>
        /// <remarks>
        /// <p>
        /// This, the default implementation, always throws a
        /// <see cref="System.IO.FileNotFoundException"/>, assuming that the
        /// resource cannot be exposed as a <see cref="System.Uri"/>.
        /// </p>
        /// </remarks>
        /// <value>
        /// The <see cref="System.Uri"/> handle for this resource.
        /// </value>
        /// <exception cref="System.IO.FileNotFoundException">
        /// This, the default implementation, <b>always</b> throws a
        /// <see cref="System.IO.FileNotFoundException"/>.
        /// </exception>
        /// <seealso cref="Spring.Core.IO.IResource.Uri"/>
        public virtual Uri Uri
        {
            get
            {
                throw new FileNotFoundException(
                    Description + " cannot be resolved to a Uri.");
            }
        }
#else
        /// <summary>
        /// Returns the <see cref="System.Uri"/> handle for this resource.
        /// </summary>
        /// <seealso cref="Spring.Core.IO.IResource.Uri"/>
        public virtual Uri Uri
        {
            get
            {
                return new Uri(resourceName);
            }
        }
#endif
        /// <summary>
        /// Returns a <see cref="System.IO.FileInfo"/> handle for this resource.
        /// </summary>
        /// <remarks>
        /// <p>
        /// This, the default implementation, always throws a
        /// <see cref="System.IO.FileNotFoundException"/>, assuming that the
        /// resource cannot be resolved to an absolute file path.
        /// </p>
        /// </remarks>
        /// <value>
        /// The <see cref="System.IO.FileInfo"/> handle for this resource.
        /// </value>
        /// <exception cref="System.IO.FileNotFoundException">
        /// This implementation <b>always</b> throws a
        /// <see cref="System.IO.FileNotFoundException"/>.
        /// </exception>
        /// <seealso cref="Spring.Core.IO.IResource"/>
        /// <see cref="Spring.Core.IO.IResource.Exists"/>
        public virtual FileInfo File
        {
            get
            {
                throw new FileNotFoundException(
                    Description + " cannot be resolved to an absolute file path.");
            }
        }

        /// <summary>
        /// Does this resource actually exist in physical form?
        /// </summary>
        /// <remarks>
        /// <p>
        /// This implementation checks whether a <see cref="System.IO.FileInfo"/>
        /// can be opened, falling back to whether a
        /// <see cref="System.IO.Stream"/> can be opened.
        /// </p>
        /// <p>
        /// This will cover both directories and content resources.
        /// </p>
        /// <p>
        /// This implementation will also return <see langword="false"/> if
        /// permission to the (file's) path is denied.
        /// </p>
        /// </remarks>
        /// <value>
        /// <see langword="true"/> if this resource actually exists in physical
        /// form (for example on a filesystem).
        /// </value>
        /// <seealso cref="Spring.Core.IO.IResource.Exists"/>
        /// <seealso cref="Spring.Core.IO.IResource.File"/>
        public virtual bool Exists
        {
            get
            {
                try
                {
                    return File.Exists;
                }
                catch (IOException)
                {
                    try
                    {
                        Stream inputStream = InputStream;
                        inputStream.Close();
                        return true;                        
                    } catch (Exception)
                    {
                        return false;
                    }
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Strips any protocol name from the supplied
        /// <paramref name="resourceName"/>.
        /// </summary>
        /// <remarks>
        /// <p>
        /// If the supplied <paramref name="resourceName"/> does not
        /// have any protocol associated with it, then the supplied
        /// <paramref name="resourceName"/> will be returned as-is.
        /// </p>
        /// </remarks>
        /// <example>
        /// <code language="C#">
        /// GetResourceNameWithoutProtocol("http://www.mycompany.com/resource.txt");
        ///		// returns www.mycompany.com/resource.txt
        /// </code>
        /// </example>
        /// <param name="resourceName">
        /// The name of the resource.
        /// </param>
        /// <returns>
        /// The name of the resource without the protocol name.
        /// </returns>
        protected static string GetResourceNameWithoutProtocol(string resourceName)
        {
            int pos = resourceName.IndexOf(
                ConfigurableResourceLoader.ProtocolSeparator);
            if (pos == -1)
            {
                return resourceName;
            }
            else
            {
                return resourceName.Substring(pos + ConfigurableResourceLoader.ProtocolSeparator.Length);
            }
        }

        /// <summary>
        /// Resolves the supplied <paramref name="resourceName"/> to its value
        /// sans any leading protocol.
        /// </summary>
        /// <param name="resourceName">
        /// The name of the resource.
        /// </param>
        /// <returns>
        /// The name of the resource without the protocol name.
        /// </returns>
        /// <see cref="Spring.Core.IO.AbstractResource.GetResourceNameWithoutProtocol"/>
        protected virtual string ResolveResourceNameWithoutProtocol(string resourceName)
        {
            return ResolveBasePathPlaceHolder(
                GetResourceNameWithoutProtocol(resourceName), BasePathPlaceHolder);
        }

        /// <summary>
        /// Resolves the presence of the
        /// <paramref name="basePathPlaceHolder"/> value
        /// in the supplied <paramref name="resourceName"/> into a path.
        /// </summary>
        /// <remarks>
        /// <p>
        /// The default implementation simply returns the supplied
        /// <paramref name="resourceName"/> as is.
        /// </p>
        /// </remarks>
        /// <param name="resourceName">
        /// The name of the resource.
        /// </param>
        /// <param name="basePathPlaceHolder">
        /// The string that is a placeholder for a base path.
        /// </param>
        /// <returns>
        /// The name of the resource with any <paramref name="basePathPlaceHolder"/>
        /// value having been resolved into an actual path.
        /// </returns>
        protected virtual string ResolveBasePathPlaceHolder(
            string resourceName, string basePathPlaceHolder)
        {
            return resourceName;
        }

        /// <summary>
        /// This implementation returns the
        /// <see cref="Description"/> of this resource.
        /// </summary>
        /// <seealso cref="Spring.Core.IO.IResource.Description"/>
        public override string ToString()
        {
            return Description;
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is
        /// equal to the current <see cref="System.Object"/>.
        /// </summary>
        /// <remarks>
        /// <p>
        /// This implementation compares <see cref="Description"/> values.
        /// </p>
        /// </remarks>
        /// <seealso cref="Spring.Core.IO.IResource.Description"/>
        public override bool Equals(object obj)
        {
            return obj is IResource
                   && ((IResource)obj).Description.Equals(Description);
        }

        /// <summary>
        /// Serves as a hash function for a particular type, suitable for use
        /// in hashing algorithms and data structures like a hash table. 
        /// </summary>
        /// <remarks>
        /// <p>
        /// This implementation returns the hashcode of the
        /// <see cref="Description"/> property.
        /// </p>
        /// </remarks>
        /// <seealso cref="Spring.Core.IO.IResource.Description"/>
        public override int GetHashCode()
        {
            return Description.GetHashCode();
        }

        #endregion

        #region Relative Resource Support

        /// <summary>
        /// Factory Method. Create a new instance of the current resource type using the given resourceName
        /// </summary>
        protected virtual IResource CreateResourceInstance( string resourceName )
        {
            return null;
        }
                
        /// <summary>
        /// The ResourceLoader to be used for resolving relative resources
        /// </summary>
        protected virtual IResourceLoader GetResourceLoader()
        {
            return new ConfigurableResourceLoader();
        }
        
        /// <summary>
        /// Does this <see cref="Spring.Core.IO.IResource"/> support relative
        /// resource retrieval?
        /// </summary>
        /// <remarks>
        /// <p>
        /// This property is generally to be consulted prior to attempting
        /// to attempting to access a resource that is relative to this
        /// resource (via a call to
        /// <see cref="Spring.Core.IO.IResource.CreateRelative"/>).
        /// </p>
        /// <p>
        /// This, the default implementation, always returns
        /// <see langword="false"/>.
        /// </p>
        /// </remarks>
        /// <value>
        /// <see langword="true"/> if this
        /// <see cref="Spring.Core.IO.IResource"/> supports relative resource
        /// retrieval.
        /// </value>
        protected virtual bool SupportsRelativeResources
        {
            get { return false; }
        }

        /// <summary>
        /// Gets the root location of the resource.
        /// </summary>
        /// <remarks>
        /// <p>
        /// Where root resource can be taken to mean that part of the resource
        /// descriptor that doesn't change when a relative resource is looked
        /// up. Examples of such a root location would include a drive letter,
        /// a web server name, an assembly name, etc.
        /// </p>
        /// </remarks>
        /// <value>
        /// The root location of the resource.
        /// </value>
        /// <exception cref="System.NotSupportedException">
        /// This, the default implementation, <b>always</b> throws a
        /// <see cref="System.NotSupportedException"/>.
        /// </exception>
        protected virtual string RootLocation
        {
            get { throw new NotSupportedException(); }
        }

        /// <summary>
        /// Gets the current path of the resource.
        /// </summary>
        /// <remarks>
        /// <p>
        /// An example value of this property would be the name of the
        /// directory containing a filesystem based resource.
        /// </p>
        /// </remarks>
        /// <value>
        /// The current path of the resource.
        /// </value>
        /// <exception cref="System.NotSupportedException">
        /// This, the default implementation, <b>always</b> throws a
        /// <see cref="System.NotSupportedException"/>.
        /// </exception>
        protected virtual string ResourcePath
        {
            get { throw new NotSupportedException(); }
        }

        /// <summary>
        /// Gets those characters that are valid path separators for the
        /// resource type.
        /// </summary>
        /// <remarks>
        /// <p>
        /// An example value of this property would be the
        /// <see cref="System.IO.Path.DirectorySeparatorChar"/> and
        /// <see cref="System.IO.Path.AltDirectorySeparatorChar"/> values for a
        /// filesystem based resource.
        /// </p>
        /// <p>
        /// Any derived classes that override this method are expected to
        /// return a new array for each access of this property.
        /// </p>
        /// </remarks>
        /// <value>
        /// Those characters that are valid path separators for the resource
        /// type.
        /// </value>
        /// <exception cref="System.NotSupportedException">
        /// This, the default implementation, <b>always</b> throws a
        /// <see cref="System.NotSupportedException"/>.
        /// </exception>
        protected virtual char[] PathSeparatorChars
        {
            get { throw new NotSupportedException(); }
        }

        /// <summary>
        /// Does the supplied <paramref name="resourceName"/> relative ?
        /// </summary>
        /// <param name="resourceName">
        /// The name of the resource to test.
        /// </param>
        /// <returns>
        /// <see langword="true"/> if resource name is relative; 
        /// otherwise <see langword="false"/>.
        /// </returns>
        protected virtual bool IsRelativeResource(string resourceName)
        {
            return false;
        }

        /// <summary>
        /// Creates a new resource that is relative to this resource based on the
        /// supplied <paramref name="resourceName"/>.
        /// </summary>
        /// <remarks>
        /// <p>
        /// This method can accept either a fully qualified resource name or a
        /// relative resource name as it's parameter.
        /// </p>
        /// <p>
        /// A fully qualified resource is one that has a protocol prefix and
        /// all elements of the resource name. All other resources are treated
        /// as relative to this resource, and the following rules are used to
        /// locate a relative resource:
        /// </p>
        /// <list type="bullet">
        ///     <item>
        ///     If the <paramref name="resourceName"/> starts with <c>'..'</c>,
        ///     the current resource path is navigated backwards before the
        ///     <paramref name="resourceName"/> is concatenated to the current
        ///     <see cref="Spring.Core.IO.AbstractResource.ResourcePath"/> of
        ///     this resource.
        ///     </item>
        ///     <item>
        ///     If the <paramref name="resourceName"/> starts with '/', the
        ///     current resource path is ignored and a new resource name is
        ///     appended to the
        ///     <see cref="Spring.Core.IO.AbstractResource.RootLocation"/> of
        ///     this resource.
        ///     </item>
        ///     <item>
        ///     If the <paramref name="resourceName"/> starts with '.' or a
        ///     letter, a new path is appended to the current 
        ///     <see cref="Spring.Core.IO.AbstractResource.ResourcePath"/> of
        ///     this resource.
        ///     </item>
        /// </list>
        /// </remarks>
        /// <param name="resourceName">
        /// The name of the resource to create.
        /// </param>
        /// <returns>The relative resource.</returns>
        /// <exception cref="System.UriFormatException">
        /// If the process of resolving the relative resource yielded an
        /// invalid URI.
        /// </exception>
        /// <exception cref="System.NotSupportedException">
        /// If this resource does not support the resolution of relative
        /// resources (as determined by the value of the
        /// <see cref="Spring.Core.IO.AbstractResource.SupportsRelativeResources"/>
        /// property).
        /// </exception>
        /// <seealso cref="Spring.Core.IO.AbstractResource.ResourcePath"/>
        public virtual IResource CreateRelative(string resourceName)
        {
            AssertUtils.ArgumentNotNull(resourceName, "relativePath");

            // try to create fully qualified resource...
            IResourceLoader loader = GetResourceLoader();
            
            if (ConfigurableResourceLoader.HasProtocol(resourceName))
            {
                IResource resource = loader.GetResource(resourceName);
                if (resource != null)
                {
                    return resource;
                }
            }
            if (!SupportsRelativeResources)
            {
                throw new NotSupportedException(GetType().Name +
                                                " does not support relative resources. Please use fully qualified resource name.");
            }

            StringBuilder fullResourceName = new StringBuilder(256);
            if (Protocol != null && Protocol != String.Empty)
            {
                fullResourceName.Append(Protocol).Append(ConfigurableResourceLoader.ProtocolSeparator);
            }
            
            if (!IsRelativeResource(resourceName))
            {
                fullResourceName.Append(resourceName);
            }
            else
            {
                string targetResource;
                string resourcePath;
                int n = resourceName.LastIndexOfAny(new char[] { '/', '\\' });
                if (n >= 0)
                {
                    targetResource = resourceName.Substring(n + 1);
                    resourcePath = CalculateResourcePath(resourceName.Substring(0, n + 1));
                }
                else // only resource name is specified, so current path should be used
                {
                    targetResource = resourceName;
                    resourcePath = ResourcePath;
                }

                fullResourceName.Append(RootLocation.TrimEnd('\\','/'));
                if (resourcePath != null && resourcePath != String.Empty)
                {
                    fullResourceName.Append('/').Append(resourcePath);
                }
                fullResourceName.Append('/').Append(targetResource);
            }

            string resultResourceName = fullResourceName.ToString();
            
            if (!ConfigurableResourceLoader.HasProtocol( resultResourceName ))
            {
                // give derived resource classes a chance to create an instance on their own
                IResource resultResource = CreateResourceInstance( resultResourceName );
                if (resultResource != null) return resultResource;
            }
            
            // create resource instance using default loader
            return loader.GetResource(resultResourceName);
        }

        /// <summary>
        /// Calculates a new resource path based on the supplied
        /// <paramref name="relativePath"/>.
        /// </summary>
        /// <param name="relativePath">
        /// The relative path to evaluate.
        /// </param>
        /// <returns>The newly calculated resource path.</returns>
        private string CalculateResourcePath(string relativePath )
        {
            StringBuilder path = new StringBuilder(256);
            if (relativePath.StartsWith("..")) // back level navigation
            {
                string[] pathElements = ResourcePath.Split(PathSeparatorChars);
                int upWalks = UpWalks(relativePath);
                if (upWalks > pathElements.Length)
                {
                    throw new UriFormatException("Too many back levels.");
                }
                char separator = PathSeparatorChars[0];
                for (int i = 0; i < pathElements.Length - upWalks; i++)
                {
                    path.Append(pathElements[i]).Append(separator);
                }
                string[] relativeParts = relativePath.Split('/', '\\');
                for (int i = upWalks; i < relativeParts.Length - 1; i++)
                {
                    path.Append(relativeParts[i]).Append(separator);
                }
                if (path.Length > 0)
                {
                    path.Length -= 1;
                }
                return path.ToString();
            }
            else if (relativePath.StartsWith("/")) // relative to root 
            {
                if (relativePath.Length > 1)
                {
                    return relativePath.Substring(1, relativePath.Length - 2);
                }
                else
                {
                    return null;
                }
            }
            else // relative to current namespace...
            {
                if (ResourcePath != null && ResourcePath != String.Empty)
                {
                    path.Append(ResourcePath.TrimEnd(PathSeparatorChars)).Append(PathSeparatorChars[0]);
                }
                if (relativePath.StartsWith("./"))
                {
                    if (relativePath.Length > 2)
                    {
                        path.Append(relativePath.Substring(2, relativePath.Length - 3));
                    }
                }
                else
                {
                    path.Append(relativePath.Substring(0, relativePath.Length - 1));
                }

                return path.ToString();
            }
        }

        private int UpWalks(string path)
        {
            string[] parts = path.Split('/', '\\');
            int count = 0;
            for (; count < parts.Length && parts[count].Equals(".."); count++)
            {
                ;
            }
            return count;
        }

        #endregion
    }
}