/*******************************************************************************
 *  Copyright 2008-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *  Licensed under the Apache License, Version 2.0 (the "License"). You may not use
 *  this file except in compliance with the License. A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 *  or in the "license" file accompanying this file.
 *  This file 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.
 * *****************************************************************************
 *    __  _    _  ___
 *   (  )( \/\/ )/ __)
 *   /__\ \    / \__ \
 *  (_)(_) \/\/  (___/
 *
 *  AWS SDK for .NET
 *
 */

using System;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;

namespace Amazon.S3.Util
{
    /// <summary>
    /// Uri wrapper that can parse out information (bucket, key, region, style) from an
    /// S3 URI.
    /// </summary>
    public class AmazonS3Uri
    {
        private const string EndpointPattern = @"^(.+\.)?s3[.-]([a-z0-9-]+)\.";

        /// <summary>
        /// True if the URI contains the bucket in the path, false if it contains the bucket in the authority.
        /// </summary>
        public bool IsPathStyle { get; private set; }

        /// <summary>
        /// The bucket name parsed from the URI (or null if no bucket specified).
        /// </summary>
        public string Bucket { get; private set; }
        
        /// <summary>
        /// The key parsed from the URI (or null if no key specified).
        /// </summary>
        public string Key { get; private set; }
        
        /// <summary>
        /// The region parsed from the URI (or null if no region specified).
        /// </summary>
        public RegionEndpoint Region { get; set; }

        /// <summary>
        /// Constructs a parser for the S3 URI specified as a string.
        /// </summary>
        /// <param name="uri">The S3 URI to be parsed.</param>
        public AmazonS3Uri(string uri)
            : this(new Uri(uri))
        {
        }

        /// <summary>
        /// Constructs a parser for the S3 URI specified as a Uri instance.
        /// </summary>
        /// <param name="uri">The S3 URI to be parsed.</param>
        public AmazonS3Uri(Uri uri)
        {
            if (uri == null)
                throw new ArgumentNullException("uri");

            if (string.IsNullOrEmpty(uri.Host))
                throw new ArgumentException("Invalid URI - no hostname present");

            var match = new Regex(EndpointPattern).Match(uri.Host);
            if (!match.Success)
                throw new ArgumentException("Invalid S3 URI - hostname does not appear to be a valid S3 endpoint");

            // for host style urls:
            //   group 0 is bucketname plus 's3' prefix and possible region code
            //   group 1 is bucket name
            //   group 2 will be region or 'amazonaws' if US Classic region
            // for path style urls:
            //   group 0 will be s3 prefix plus possible region code
            //   group 1 will be empty
            //   group 2 will be region or 'amazonaws' if US Classic region
            var bucketNameGroup = match.Groups[1];
            if (string.IsNullOrEmpty(bucketNameGroup.Value))
            {
                // no bucket name in the authority, parse it from the path
                this.IsPathStyle = true;

                // grab the encoded path so we don't run afoul of '/'s in the bucket name
                var path = uri.AbsolutePath;
                if (path.Equals("/"))
                {
                    this.Bucket = null;
                    this.Key = null;
                }
                else
                {
                    var index = path.IndexOf('/', 1);
                    if (index == -1)
                    {
                        // https://s3.amazonaws.com/bucket
                        this.Bucket = Decode(path.Substring(1));
                        this.Key = null;
                    }
                    else if (index == (path.Length - 1))
                    {
                        // https://s3.amazonaws.com/bucket/
                        this.Bucket = Decode(path.Substring(1, index)).TrimEnd('/');
                        this.Key = null;
                    }
                    else
                    {
                        // https://s3.amazonaws.com/bucket/key
                        this.Bucket = Decode(path.Substring(1, index)).TrimEnd('/');
                        this.Key = Decode(path.Substring(index + 1));
                    }
                }
            }
            else
            {
                // bucket name in the host, path is the object key
                this.IsPathStyle = false;

                // remove any trailing '.' from the prefix to get the bucket name
                this.Bucket = bucketNameGroup.Value.TrimEnd('.');
                this.Key = uri.AbsolutePath.Equals("/") ? null : uri.AbsolutePath.Substring(1);
            }

            if (match.Groups.Count > 2)
            {
                // US 'classic' urls will not have a region code in the endpoint
                var regionGroupValue = match.Groups[2].Value;
                if (regionGroupValue.Equals("amazonaws", StringComparison.Ordinal)
                    || regionGroupValue.Equals("external-1", StringComparison.Ordinal))
                    this.Region = RegionEndpoint.USEast1;
                else
                {
                    try
                    {
                        this.Region = RegionEndpoint.GetBySystemName(regionGroupValue);
                    }
                    catch (Amazon.Runtime.AmazonClientException)
                    {
                        // in cases where endpoints such as "s3-accelerate" matches, 
                        // just set the region to null and move on.
                        this.Region = null;
                    }
                }
                    
            }
        }

        /// <summary>
        /// If the given string is an AmazonS3Endpoint return true and set the AmazonS3Uri out parameter.
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="amazonS3Uri"></param>
        /// <returns>true if the string is an AmazonS3Endpoint, and the out paramter has been filled in, false otherwise</returns>
        public static bool TryParseAmazonS3Uri(string uri, out AmazonS3Uri amazonS3Uri)
        {
            return TryParseAmazonS3Uri(new Uri(uri), out amazonS3Uri);
        }

        /// <summary>
        /// If the given Uri is an AmazonS3Endpoint return true and set the AmazonS3Uri out parameter.
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="amazonS3Uri"></param>
        /// <returns>true if the Uri is an AmazonS3Endpoint, and the out paramter has been filled in, false otherwise</returns>
        public static bool TryParseAmazonS3Uri(Uri uri, out AmazonS3Uri amazonS3Uri)
        {
            if (IsAmazonS3Endpoint(uri))
            {
                amazonS3Uri = new AmazonS3Uri(uri);
                return true;
            }
            else
            {
                amazonS3Uri = null;
                return false;
            }
        }

        /// <summary>
        /// Checks whether the given URI is a Amazon S3 URI.
        /// </summary>
        /// <param name="uri">The S3 URI to be checked.</param>
        /// <returns>true if the URI is a Amazon S3 URI, false; otherwise.</returns>
        public static bool IsAmazonS3Endpoint(string uri)
        {
            if (uri == null)
                throw new ArgumentNullException("uri");

            return IsAmazonS3Endpoint(new Uri(uri));
        }

        /// <summary>
        /// Checks whether the given URI is a Amazon S3 URI.
        /// </summary>
        /// <param name="uri">The S3 URI to be checked.</param>
        /// <returns>true if the URI is a Amazon S3 URI, false; otherwise.</returns>
        public static bool IsAmazonS3Endpoint(Uri uri)
        {
            if (uri == null)
                throw new ArgumentNullException("uri");

            var match = new Regex(EndpointPattern).Match(uri.Host);
            if (uri.Host.EndsWith("amazonaws.com", StringComparison.OrdinalIgnoreCase) || uri.Host.EndsWith("amazonaws.com.cn", StringComparison.OrdinalIgnoreCase))
            {
                return match.Success;
            }
            else
            {
                return false;
            }

        }

        /// <summary>
        /// Percent-decodes the given string, with a fast path for strings that are not
        /// percent-encoded.
        /// </summary>
        /// <param name="s">The string to decode</param>
        /// <returns>The decoded string</returns>
        static string Decode(string s)
        {
            if (s == null)
                return null;

            for (var i = 0; i < s.Length; ++i)
            {
                if (s[i] == '%')
                    return Decode(s, i);
            }

            return s;
        }

        /// <summary>
        /// Percent-decodes the given string.
        /// </summary>
        /// <param name="s">The string to decode</param>
        /// <param name="firstPercent">The index of the first '%' in the string</param>
        /// <returns>The decoded string</returns>
        static string Decode(string s, int firstPercent)
        {
            var sb = new StringBuilder(s.Substring(0, firstPercent));

            AppendDecoded(sb, s, firstPercent);

            for (var i = firstPercent + 3; i < s.Length; ++i)
            {
                if (s[i] == '%')
                {
                    AppendDecoded(sb, s, i);
                    i += 2;
                }
                else
                    sb.Append(s[i]);
            }

            return sb.ToString();
        }

        /// <summary>
        /// Decodes the percent-encoded character at the given index in the string
        /// and appends the decoded value to the string under construction.
        /// </summary>
        /// <param name="builder">
        /// The string under construction to which the decoded character will be 
        /// appended.
        /// </param>
        /// <param name="s">The string being decoded.</param>
        /// <param name="index">The index of the '%' character in the string.</param>
        static void AppendDecoded(StringBuilder builder, string s, int index)
        {
            if (index > s.Length - 3)
                throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, 
                                                                  "Invalid percent-encoded string '{0}'", 
                                                                  s));

            var first = s[index + 1];
            var second = s[index + 2];

            var decoded = (char) (FromHex(first) << 4 | FromHex(second));
            builder.Append(decoded);
        }

        /// <summary>
        /// Converts a hex character (0-9A-Fa-f) into its corresponding quad value.
        /// </summary>
        /// <param name="c">The hex character</param>
        /// <returns>The quad value</returns>
        static int FromHex(char c)
        {
            if (c < '0')
            {
                throw new InvalidOperationException(
                    "Invalid percent-encoded string: bad character '" + c + "' in "
                    + "escape sequence.");
            }
            if (c <= '9')
            {
                return (c - '0');
            }

            if (c < 'A')
            {
                throw new InvalidOperationException(
                    "Invalid percent-encoded string: bad character '" + c + "' in "
                    + "escape sequence.");
            }
            if (c <= 'F')
            {
                return (c - 'A') + 10;
            }

            if (c < 'a')
            {
                throw new InvalidOperationException(
                    "Invalid percent-encoded string: bad character '" + c + "' in "
                    + "escape sequence.");
            }
            if (c <= 'f')
            {
                return (c - 'a') + 10;
            }

            throw new InvalidOperationException(
                "Invalid percent-encoded string: bad character '" + c + "' in "
                + "escape sequence.");
        }
    }
}