/*
 * Copyright 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.
 */
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Net;
using Amazon.Runtime;
using Amazon.Runtime.Internal.Util;
using Amazon.Runtime.Internal.Auth;
using Amazon.Util;
namespace Amazon.Runtime.Internal
{
    /// 
    /// Default implementation of the IRequest interface.
    /// 
    /// This class is only intended for internal use inside the AWS client libraries.
    /// Callers shouldn't ever interact directly with objects of this class.
    /// 
    /// 
    public class DefaultRequest : IRequest
    {
        readonly ParameterCollection parametersCollection;
        readonly IDictionary parametersFacade;
        readonly IDictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
        readonly IDictionary trailingHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
        readonly IDictionary subResources = new Dictionary(StringComparer.OrdinalIgnoreCase);
        readonly IDictionary pathResources = new Dictionary(StringComparer.Ordinal);
        Uri endpoint;
        string resourcePath;
        string serviceName;
        readonly AmazonWebServiceRequest originalRequest;
        byte[] content;
        Stream contentStream;
        string contentStreamHash;
        string httpMethod = "POST";
        bool useQueryString = false;
        string requestName;
        string canonicalResource;
        RegionEndpoint alternateRegion;        
        long originalStreamLength;
        int marshallerVersion = 2; //2 is the default version and must be used whenever a version is not specified in the marshaller.
        /// 
        /// Constructs a new DefaultRequest with the specified service name and the
        /// original, user facing request object.
        /// 
        /// The orignal request that is being wrapped
        /// The service name
        public DefaultRequest(AmazonWebServiceRequest request, String serviceName)
        {
            if (request == null) throw new ArgumentNullException("request");
            if (string.IsNullOrEmpty(serviceName)) throw new ArgumentNullException("serviceName");
            this.serviceName = serviceName;
            this.originalRequest = request;
            this.requestName = this.originalRequest.GetType().Name;
            this.UseSigV4 = ((Amazon.Runtime.Internal.IAmazonWebServiceRequest)this.originalRequest).UseSigV4;
            this.SignatureVersion = ((Amazon.Runtime.Internal.IAmazonWebServiceRequest)this.originalRequest).SignatureVersion;
            this.HostPrefix = string.Empty;
            parametersCollection = new ParameterCollection();
            parametersFacade = new ParametersDictionaryFacade(parametersCollection);
        }
        /// 
        /// The name of the request
        /// 
        public string RequestName
        {
            get { return this.requestName; }
        }
        /// 
        /// Gets and sets the type of http request to make, whether it should be POST,GET or DELETE
        /// 
        public string HttpMethod
        {
            get
            {
                return this.httpMethod;
            }
            set
            {
                this.httpMethod = value;
            }
        }
        /// 
        /// Gets and sets a flag that indicates whether the request is sent as a query string instead of the request body.
        /// 
        public bool UseQueryString
        {
            get
            {
                if (this.HttpMethod == "GET")
                    return true;
                return this.useQueryString;
            }
            set
            {
                this.useQueryString = value;
            }
        }
        /// 
        /// Returns the original, user facing request object which this internal
        /// request object is representing.
        /// 
        public AmazonWebServiceRequest OriginalRequest
        {
            get
            {
                return originalRequest;
            }
        }
        /// 
        /// Returns a dictionary of the headers included in this request.
        /// 
        public IDictionary Headers
        {
            get
            {
                return this.headers;
            }
        }
        /// 
        /// Returns a dictionary of the parameters included in this request.
        /// 
        public IDictionary Parameters
        {
            get
            {
                return this.parametersFacade;
            }
        }
        /// 
        /// Collection of parameters included in this request.
        /// 
        public ParameterCollection ParameterCollection
        {
            get
            {
                return this.parametersCollection;
            }
        }
        /// 
        /// Returns the subresources that should be appended to the resource path.
        /// This is used primarily for Amazon S3, where object keys can contain '?'
        /// characters, making string-splitting of a resource path potentially 
        /// hazardous.
        /// 
        public IDictionary SubResources
        {
            get
            {
                return this.subResources;
            }
        }
        /// 
        /// Adds a new null entry to the SubResources collection for the request
        /// 
        /// The name of the subresource
        public void AddSubResource(string subResource)
        {
            AddSubResource(subResource, null);
        }
        /// 
        /// Adds a new entry to the SubResources collection for the request
        /// 
        /// The name of the subresource
        /// Value of the entry
        public void AddSubResource(string subResource, string value)
        {
            SubResources.Add(subResource, value);
        }
        /// 
        /// Gets and Sets the endpoint for this request.
        /// 
        public Uri Endpoint
        {
            get
            {
                return this.endpoint;
            }
            set
            {
                this.endpoint = value;
            }
        }
        /// 
        /// Gets and Sets the resource path added on to the endpoint.
        /// 
        public string ResourcePath
        {
            get
            {
                return this.resourcePath;
            }
            set
            {
                this.resourcePath = value;
            }
        }
        /// 
        /// Returns the path resources that should be used within the resource path.
        /// This is used for services where path keys can contain '/'
        /// characters, making string-splitting of a resource path potentially 
        /// hazardous.
        /// 
        public IDictionary PathResources
        {
            get
            {
                return this.pathResources;
            }
        }
        /// 
        /// Adds a new entry to the PathResources collection for the request
        /// 
        /// The name of the pathresource with potential greedy syntax: {key+}
        /// Value of the entry
        public void AddPathResource(string key, string value)
        {
            PathResources.Add(key, value);
        }
        /// 
        /// Gets and Sets the version number for the marshaller used to create this request. The version number
        /// is used to support backward compatible changes that would otherwise be breaking changes when a 
        /// newer core is used with an older service assembly.
        /// Versions:
        ///     1 - Legacy version (no longer supported)
        ///     2 - Default version. Support for path segments
        /// 
        public int MarshallerVersion
        {
            get
            {
                return this.marshallerVersion;
            }
            set
            {
                this.marshallerVersion = value;
            }
        }
        public string CanonicalResource
        {
            get
            {
                return this.canonicalResource;
            }
            set
            {
                this.canonicalResource = value;
            }
        }
        /// 
        /// Gets and Sets the content for this request.
        /// 
        public byte[] Content
        {
            get
            {
                return this.content;
            }
            set
            {
                this.content = value;
            }
        }
        /// 
        /// Flag that signals that Content was and should be set
        /// from the Parameters collection.
        /// 
        public bool SetContentFromParameters { get; set; }
        /// 
        /// Gets and sets the content stream.
        /// 
        public Stream ContentStream
        {
            get { return this.contentStream; }
            set
            {
                this.contentStream = value;
                OriginalStreamPosition = -1;
                if (this.contentStream != null)
                {
                    Stream baseStream = HashStream.GetNonWrapperBaseStream(this.contentStream);
                    if (baseStream.CanSeek)
                        OriginalStreamPosition = baseStream.Position;
                }
            }
        }
        /// 
        /// Gets and sets the original stream position.
        /// If ContentStream is null or does not support seek, this propery
        /// should be equal to -1.
        /// 
        public long OriginalStreamPosition
        {
            get { return this.originalStreamLength; }
            set { this.originalStreamLength = value; }
        }
        /// 
        /// Computes the SHA 256 hash of the content stream. If the stream is not
        /// seekable, it searches the parent stream hierarchy to find a seekable
        /// stream prior to computation. Once computed, the hash is cached for future
        /// use. If a suitable stream cannot be found to use, null is returned.
        /// 
        public string ComputeContentStreamHash()
        {
            if (this.contentStream == null)
                return null;
            if (this.contentStreamHash == null)
            {
                var seekableStream = WrapperStream.SearchWrappedStream(this.contentStream, s => s.CanSeek);
                if (seekableStream != null)
                {
                    var position = seekableStream.Position;
                    byte[] payloadHashBytes = CryptoUtilFactory.CryptoInstance.ComputeSHA256Hash(seekableStream);
                    this.contentStreamHash = AWSSDKUtils.ToHex(payloadHashBytes, true);
                    seekableStream.Seek(position, SeekOrigin.Begin);
                }
            }
            return this.contentStreamHash;
        }
        /// 
        /// The name of the service to which this request is being sent.
        /// 
        public string ServiceName
        {
            get
            {
                return this.serviceName;
            }
        }
        /// 
        /// Alternate endpoint to use for this request, if any.
        /// 
        public RegionEndpoint AlternateEndpoint
        {
            get
            {
                return this.alternateRegion;
            }
            set
            {
                this.alternateRegion = value;
            }
        }
        /// 
        /// Host prefix value to prepend to the endpoint for this request, if any.
        /// 
        public string HostPrefix { get; set; }
        /// 
        /// Gets and sets the Suppress404Exceptions property. If true then 404s return back from AWS will not cause an exception and 
        /// an empty response object will be returned.
        /// 
        public bool Suppress404Exceptions
        {
            get;
            set;
        }
        /// 
        /// If using AWS4 signing protocol, contains the resultant parts of the
        /// signature that we may need to make use of if we elect to do a chunked
        /// encoding upload.
        /// 
        public AWS4SigningResult AWS4SignerResult { get; set; }
        ///       
        /// WARNING: Setting DisablePayloadSigning to true disables the SigV4 payload signing 
        /// data integrity check on this request.  
        /// If using SigV4, the DisablePayloadSigning flag controls if the payload should be 
        /// signed on a request by request basis. By default this flag is null which will use the 
        /// default client behavior. The default client behavior is to sign the payload. When 
        /// DisablePayloadSigning is true, the request will be signed with an UNSIGNED-PAYLOAD value. 
        /// Setting DisablePayloadSigning to true requires that the request is sent over a HTTPS 
        /// connection.        
        /// Under certain circumstances, such as uploading to S3 while using MD5 hashing, it may 
        /// be desireable to use UNSIGNED-PAYLOAD to decrease signing CPU usage. This flag only applies 
        /// to Amazon S3 PutObject and UploadPart requests.
        /// MD5Stream, SigV4 payload signing, and HTTPS each provide some data integrity 
        /// verification. If DisableMD5Stream is true and DisablePayloadSigning is true, then the 
        /// possibility of data corruption is completely dependant on HTTPS being the only remaining 
        /// source of data integrity verification.
        /// 
        public bool? DisablePayloadSigning { get; set; }
        /// 
        /// If using SigV4a signing protocol, contains the resultant parts of the
        /// signature that we may need to make use of if we elect to do a chunked
        /// encoding upload.
        /// 
        public AWS4aSigningResult AWS4aSignerResult { get; set; }
        /// 
        /// Determine whether to use a chunked encoding upload for the request
        /// (applies to Amazon S3 PutObject and UploadPart requests only).  If 
        /// DisablePayloadSigning is true, UseChunkEncoding will be automatically 
        /// set to false.
        /// 
        public bool UseChunkEncoding { get; set; }
        /// 
        /// Determine whether to use double encoding for request's signer.
        /// Propagated from the endpoint's authSchemes.
        /// Default value is "true".
        /// Currently only S3 and S3 Control will disable double encoding.
        /// 
        public bool UseDoubleEncoding { get; set; } = true;
        /// 
        /// Used for Amazon S3 requests where the bucket name is removed from
        /// the marshalled resource path into the host header. To comply with
        /// AWS2 signature calculation, we need to recover the bucket name
        /// and include it in the resource canonicalization, which we do using
        /// this field.
        /// 
        public string CanonicalResourcePrefix
        {
            get;
            set;
        }
        /// 
        /// This flag specifies if SigV4 is required for the current request.
        /// Returns true if the request will use SigV4.
        /// Setting it to false will use SigV2.
        /// 
        [Obsolete("UseSigV4 is deprecated. Use SignatureVersion directly instead.")]
        public bool UseSigV4 
        {   
            get { return SignatureVersion == SignatureVersion.SigV4; }
            set { this.SignatureVersion = value ? SignatureVersion.SigV4 : SignatureVersion.SigV2; }
        }
        /// 
        /// Specifies which signature version shall be used for the current request.
        /// 
        public SignatureVersion SignatureVersion { get; set; }
        /// 
        /// The authentication region to use for the request.
        /// Set from Config.AuthenticationRegion.
        /// 
        public string AuthenticationRegion { get; set; }
        /// 
        /// The region in which the service request was signed.
        /// 
        public string DeterminedSigningRegion { get ; set; }
        /// 
        /// If the request needs to be signed with a different service name 
        /// than the client config AuthenticationServiceName, set it here to override
        /// the result of DetermineService in AWS4Signer
        /// 
        public string OverrideSigningServiceName { get; set; }
        /// 
        /// The checksum algorithm that was selected to validate this request's integrity
        /// 
        public CoreChecksumAlgorithm SelectedChecksum { get; set; }
        /// 
        /// Returns a dictionary of the trailing headers included
        /// after this request's content.
        /// 
        public IDictionary TrailingHeaders => this.trailingHeaders;
        /// 
        /// Checks if the request stream can be rewinded.
        /// 
        /// Returns true if the request stream can be rewinded ,
        /// else false.
        public bool IsRequestStreamRewindable()
        {
            var stream = this.ContentStream;
            // Retries may not be possible with a stream
            if (stream != null)
            {
                // Pull out the underlying non-wrapper stream
                stream = WrapperStream.GetNonWrapperBaseStream(stream);
                // Retry is possible if stream is seekable
                return stream.CanSeek;
            }
            return true;
        }
        /// 
        /// Returns true if the request can contain a request body, else false.
        /// 
        /// Returns true if the currect request can contain a request body, else false.
        public bool MayContainRequestBody()
        {
            return
                (this.HttpMethod == "POST" ||
                 this.HttpMethod == "PUT" ||
                 this.HttpMethod == "PATCH");
        }
        /// 
        /// Returns true if the request has a body, else false.
        /// 
        /// Returns true if the request has a body, else false.
        public bool HasRequestBody()
        {
            var isPutPost = (this.HttpMethod == "POST" || this.HttpMethod == "PUT" || this.HttpMethod == "PATCH");
            var hasContent = this.HasRequestData();
            return (isPutPost && hasContent);
        }
        public string GetHeaderValue(string headerName)
        {
            string headerValue;
            if (headers.TryGetValue(headerName, out headerValue))
                return headerValue;
            return string.Empty;
        }
    }
}