//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------
namespace System.ServiceModel.Transactions
{
    using System;
    using System.Runtime;
    using System.Security.Permissions;
    using System.ServiceModel.Channels;
    using System.ServiceModel.Security;
    using System.Transactions;
    using System.Xml;
    using Microsoft.Transactions.Wsat.Messaging;
    using Microsoft.Transactions.Wsat.Protocol;
    using DiagnosticUtility = System.ServiceModel.DiagnosticUtility;

    abstract class WsatTransactionFormatter : TransactionFormatter
    {
        bool initialized;
        WsatConfiguration wsatConfig;
        WsatProxy wsatProxy;
        ProtocolVersion protocolVersion;

        protected WsatTransactionFormatter(ProtocolVersion protocolVersion)
        {
            this.protocolVersion = protocolVersion;
        }

        //=======================================================================================
        void EnsureInitialized()
        {
            if (!this.initialized)
            {
                lock (this)
                {
                    if (!this.initialized)
                    {
                        this.wsatConfig = new WsatConfiguration();
                        this.wsatProxy = new WsatProxy(this.wsatConfig, this.protocolVersion);
                        this.initialized = true;
                    }
                }
            }
        }

        //=======================================================================================
        // The demand is not added now (in 4.5), to avoid a breaking change. To be considered in the next version.
        /*
        [PermissionSet(SecurityAction.Demand, Unrestricted = true)] // because we call code from a non-APTCA assembly; WSATs are not supported in partial trust, so customers should not be broken by this demand
        */
        public override void WriteTransaction(Transaction transaction, Message message)
        {
            EnsureInitialized();

            ForcePromotion(transaction);

            // Make a context and add it to the message
            CoordinationContext context;
            RequestSecurityTokenResponse issuedToken;
            MarshalAsCoordinationContext(transaction, out context, out issuedToken);
            if (issuedToken != null)
            {
                CoordinationServiceSecurity.AddIssuedToken(message, issuedToken);
            }

            WsatTransactionHeader header = new WsatTransactionHeader(context, this.protocolVersion);
            message.Headers.Add(header);
        }

        //=======================================================================================
        void ForcePromotion(Transaction transaction)
        {
            // Force promotion. This may throw TransactionException.
            // We used to check the DistributedIdentifier property first, but VSWhidbey bug 547901 
            // prevents us from doing so reliably in multi-threaded scenarios (there is a ----
            // in the System.Transactions code that can cause a NullReferenceException if we ask
            // for the identifier while the transaction is being promoted)
            TransactionInterop.GetTransmitterPropagationToken(transaction);
        }

        //=======================================================================================
        // The demand is not added now (in 4.5), to avoid a breaking change. To be considered in the next version.
        /*
        // We demand full trust because we use CoordinationServiceSecurity from a non-APTCA assembly and CoordinationServiceSecurity.GetIssuedToken(..) can call Environment.FailFast.
        // It's recommended to not let partially trusted callers to bring down the process.
        // WSATs are not supported in partial trust, so customers should not be broken by this demand.
        [PermissionSet(SecurityAction.Demand, Unrestricted = true)]
        */
        public override TransactionInfo ReadTransaction(Message message)
        {
            EnsureInitialized();

            CoordinationContext context = WsatTransactionHeader.GetCoordinationContext(message, this.protocolVersion);
            if (context == null)
                return null;

            // Incoming transaction tokens are optional
            RequestSecurityTokenResponse issuedToken;
            try
            {
                issuedToken = CoordinationServiceSecurity.GetIssuedToken(message, context.Identifier, this.protocolVersion);
            }
            catch (XmlException e)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(
                    new TransactionException(SR.FailedToDeserializeIssuedToken, e));
            }

            return new WsatTransactionInfo(this.wsatProxy, context, issuedToken);
        }

        //=======================================================================================
        public WsatTransactionInfo CreateTransactionInfo(CoordinationContext context,
                                                         RequestSecurityTokenResponse issuedToken)
        {
            return new WsatTransactionInfo(this.wsatProxy, context, issuedToken);
        }

        //=======================================================================================
        // The demand is not added now (in 4.5), to avoid a breaking change. To be considered in the next version.
        /*
        // We demand full trust because we use CoordinationContext and CoordinationServiceSecurity from a non-APTCA assembly.
        // The CoordinationContext constructor can call Environment.FailFast and it's recommended to not let partially trusted callers to bring down the process.
        // WSATs are not supported in partial trust, so customers should not be broken by this demand.
        [PermissionSet(SecurityAction.Demand, Unrestricted = true)]
        */
        public void MarshalAsCoordinationContext(Transaction transaction,
                                                 out CoordinationContext context,
                                                 out RequestSecurityTokenResponse issuedToken)
        {
            Guid transactionId = transaction.TransactionInformation.DistributedIdentifier;
            string nonNativeContextId = null;

            context = new CoordinationContext(this.protocolVersion);

            // Get timeout, description and isolation flags
            uint timeout;
            IsolationFlags isoFlags;
            string description;
            OleTxTransactionFormatter.GetTransactionAttributes(transaction,
                                                               out timeout,
                                                               out isoFlags,
                                                               out description);
            context.IsolationFlags = isoFlags;
            context.Description = description;

            // If we can, use cached extended information
            // Note - it may be worth using outgoing contexts more than once.
            // We'll let performance profiling decide that question
            WsatExtendedInformation info;
            if (WsatExtendedInformationCache.Find(transaction, out info))
            {
                context.Expires = info.Timeout;

                // The extended info cache only contains an identifier when it's non-native
                if (!string.IsNullOrEmpty(info.Identifier))
                {
                    context.Identifier = info.Identifier;
                    nonNativeContextId = info.Identifier;
                }
            }
            else
            {
                context.Expires = timeout;
                if (context.Expires == 0)
                {
                    // If the timeout is zero, there are two possibilities:
                    // 1) This is a root transaction with an infinite timeout.
                    // 2) This is a subordinate transaction whose timeout was not flowed.
                    // We have no mechanism for distinguishing between the two cases.
                    //
                    // We could always return zero here, instead of using the local max timeout.
                    // The problem is that the 2004/08 WS-C spec does not specify the meaning
                    // of a zero expires field. While we accept zero to mean "as large as possible"
                    // it would be risky to expect others to do the same.  So we only propagate
                    // zero in the expires field if the local max timeout has been disabled.
                    //
                    // This is MB 34596: how can we flow the real timeout?
                    context.Expires = (uint)TimeoutHelper.ToMilliseconds(this.wsatConfig.MaxTimeout);
                }
            }

            if (context.Identifier == null)
            {
                context.Identifier = CoordinationContext.CreateNativeIdentifier(transactionId);
                nonNativeContextId = null;
            }

            string tokenId;
            if (!this.wsatConfig.IssuedTokensEnabled)
            {
                tokenId = null;
                issuedToken = null;
            }
            else
            {
                CoordinationServiceSecurity.CreateIssuedToken(transactionId,
                                                              context.Identifier,
                                                              this.protocolVersion,
                                                              out issuedToken,
                                                              out tokenId);
            }

            AddressHeader refParam = new WsatRegistrationHeader(transactionId, nonNativeContextId, tokenId);
            context.RegistrationService = wsatConfig.CreateRegistrationService(refParam, this.protocolVersion);
            context.IsolationLevel = transaction.IsolationLevel;
            context.LocalTransactionId = transactionId;

            if (this.wsatConfig.OleTxUpgradeEnabled)
            {
                context.PropagationToken = TransactionInterop.GetTransmitterPropagationToken(transaction);
            }
        }
    }

    //------------------------------------------------------------------------------------------
    //                          Versioned Wsat transaction formatters
    //------------------------------------------------------------------------------------------

    class WsatTransactionFormatter10 : WsatTransactionFormatter
    {
        static WsatTransactionHeader emptyTransactionHeader = new WsatTransactionHeader(null, ProtocolVersion.Version10);

        public WsatTransactionFormatter10() : base(ProtocolVersion.Version10) { }

        //=======================================================================================
        public override MessageHeader EmptyTransactionHeader
        {
            get { return emptyTransactionHeader; }
        }
    }

    class WsatTransactionFormatter11 : WsatTransactionFormatter
    {
        static WsatTransactionHeader emptyTransactionHeader = new WsatTransactionHeader(null, ProtocolVersion.Version11);

        public WsatTransactionFormatter11() : base(ProtocolVersion.Version11) { }

        //=======================================================================================
        public override MessageHeader EmptyTransactionHeader
        {
            get { return emptyTransactionHeader; }
        }
    }
}
