using System;
using System.Collections;
using System.IO;

using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;

namespace Org.BouncyCastle.Crypto.Tls
{
    public class TlsClientProtocol
        :   TlsProtocol
    {
        protected TlsClient mTlsClient = null;
        internal TlsClientContextImpl mTlsClientContext = null;

        protected byte[] mSelectedSessionID = null;

        protected TlsKeyExchange mKeyExchange = null;
        protected TlsAuthentication mAuthentication = null;

        protected CertificateStatus mCertificateStatus = null;
        protected CertificateRequest mCertificateRequest = null;

        /**
         * Constructor for blocking mode.
         * @param stream The bi-directional stream of data to/from the server
         * @param secureRandom Random number generator for various cryptographic functions
         */
        public TlsClientProtocol(Stream stream, SecureRandom secureRandom)
            : base(stream, secureRandom)
        {
        }

        /**
         * Constructor for blocking mode.
         * @param input The stream of data from the server
         * @param output The stream of data to the server
         * @param secureRandom Random number generator for various cryptographic functions
         */
        public TlsClientProtocol(Stream input, Stream output, SecureRandom secureRandom)
            : base(input, output, secureRandom)
        {
        }

        /**
         * Constructor for non-blocking mode.<br/>
         * <br/>
         * When data is received, use {@link #offerInput(java.nio.ByteBuffer)} to
         * provide the received ciphertext, then use
         * {@link #readInput(byte[], int, int)} to read the corresponding cleartext.<br/>
         * <br/>
         * Similarly, when data needs to be sent, use
         * {@link #offerOutput(byte[], int, int)} to provide the cleartext, then use
         * {@link #readOutput(byte[], int, int)} to get the corresponding
         * ciphertext.
         * 
         * @param secureRandom
         *            Random number generator for various cryptographic functions
         */
        public TlsClientProtocol(SecureRandom secureRandom)
            : base(secureRandom)
        {
        }

        /**
         * Initiates a TLS handshake in the role of client.<br/>
         * <br/>
         * In blocking mode, this will not return until the handshake is complete.
         * In non-blocking mode, use {@link TlsPeer#NotifyHandshakeComplete()} to
         * receive a callback when the handshake is complete.
         *
         * @param tlsClient The {@link TlsClient} to use for the handshake.
         * @throws IOException If in blocking mode and handshake was not successful.
         */
        public virtual void Connect(TlsClient tlsClient)
        {
            if (tlsClient == null)
                throw new ArgumentNullException("tlsClient");
            if (this.mTlsClient != null)
                throw new InvalidOperationException("'Connect' can only be called once");

            this.mTlsClient = tlsClient;

            this.mSecurityParameters = new SecurityParameters();
            this.mSecurityParameters.entity = ConnectionEnd.client;

            this.mTlsClientContext = new TlsClientContextImpl(mSecureRandom, mSecurityParameters);

            this.mSecurityParameters.clientRandom = CreateRandomBlock(tlsClient.ShouldUseGmtUnixTime(),
                mTlsClientContext.NonceRandomGenerator);

            this.mTlsClient.Init(mTlsClientContext);
            this.mRecordStream.Init(mTlsClientContext);

            tlsClient.NotifyCloseHandle(this);

            TlsSession sessionToResume = tlsClient.GetSessionToResume();
            if (sessionToResume != null && sessionToResume.IsResumable)
            {
                SessionParameters sessionParameters = sessionToResume.ExportSessionParameters();
                if (sessionParameters != null && sessionParameters.IsExtendedMasterSecret)
                {
                    this.mTlsSession = sessionToResume;
                    this.mSessionParameters = sessionParameters;
                }
            }

            SendClientHelloMessage();
            this.mConnectionState = CS_CLIENT_HELLO;

            BlockForHandshake();
        }

        protected override void CleanupHandshake()
        {
            base.CleanupHandshake();

            this.mSelectedSessionID = null;
            this.mKeyExchange = null;
            this.mAuthentication = null;
            this.mCertificateStatus = null;
            this.mCertificateRequest = null;
        }

        protected override TlsContext Context
        {
            get { return mTlsClientContext; }
        }

        internal override AbstractTlsContext ContextAdmin
        {
            get { return mTlsClientContext; }
        }

        protected override TlsPeer Peer
        {
            get { return mTlsClient; }
        }

        protected override void HandleHandshakeMessage(byte type, MemoryStream buf)
        {
            if (this.mResumedSession)
            {
                if (type != HandshakeType.finished || this.mConnectionState != CS_SERVER_HELLO)
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);

                ProcessFinishedMessage(buf);
                this.mConnectionState = CS_SERVER_FINISHED;

                SendChangeCipherSpecMessage();
                SendFinishedMessage();
                this.mConnectionState = CS_CLIENT_FINISHED;

                CompleteHandshake();
                return;
            }

            switch (type)
            {
            case HandshakeType.certificate:
            {
                switch (this.mConnectionState)
                {
                case CS_SERVER_HELLO:
                case CS_SERVER_SUPPLEMENTAL_DATA:
                {
                    if (this.mConnectionState == CS_SERVER_HELLO)
                    {
                        HandleSupplementalData(null);
                    }

                    // Parse the Certificate message and Send to cipher suite

                    this.mPeerCertificate = Certificate.Parse(buf);

                    AssertEmpty(buf);

                    // TODO[RFC 3546] Check whether empty certificates is possible, allowed, or excludes CertificateStatus
                    if (this.mPeerCertificate == null || this.mPeerCertificate.IsEmpty)
                    {
                        this.mAllowCertificateStatus = false;
                    }

                    this.mKeyExchange.ProcessServerCertificate(this.mPeerCertificate);

                    this.mAuthentication = mTlsClient.GetAuthentication();
                    this.mAuthentication.NotifyServerCertificate(this.mPeerCertificate);

                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                this.mConnectionState = CS_SERVER_CERTIFICATE;
                break;
            }
            case HandshakeType.certificate_status:
            {
                switch (this.mConnectionState)
                {
                case CS_SERVER_CERTIFICATE:
                {
                    if (!this.mAllowCertificateStatus)
                    {
                        /*
                         * RFC 3546 3.6. If a server returns a "CertificateStatus" message, then the
                         * server MUST have included an extension of type "status_request" with empty
                         * "extension_data" in the extended server hello..
                         */
                        throw new TlsFatalAlert(AlertDescription.unexpected_message);
                    }

                    this.mCertificateStatus = CertificateStatus.Parse(buf);

                    AssertEmpty(buf);

                    // TODO[RFC 3546] Figure out how to provide this to the client/authentication.

                    this.mConnectionState = CS_CERTIFICATE_STATUS;
                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }
                break;
            }
            case HandshakeType.finished:
            {
                switch (this.mConnectionState)
                {
                case CS_CLIENT_FINISHED:
                case CS_SERVER_SESSION_TICKET:
                {
                    if (this.mConnectionState == CS_CLIENT_FINISHED && this.mExpectSessionTicket)
                    {
                        /*
                         * RFC 5077 3.3. This message MUST be sent if the server included a
                         * SessionTicket extension in the ServerHello.
                         */
                        throw new TlsFatalAlert(AlertDescription.unexpected_message);
                    }

                    ProcessFinishedMessage(buf);
                    this.mConnectionState = CS_SERVER_FINISHED;

                    CompleteHandshake();
                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }
                break;
            }
            case HandshakeType.server_hello:
            {
                switch (this.mConnectionState)
                {
                case CS_CLIENT_HELLO:
                {
                    ReceiveServerHelloMessage(buf);
                    this.mConnectionState = CS_SERVER_HELLO;

                    this.mRecordStream.NotifyHelloComplete();

                    ApplyMaxFragmentLengthExtension();

                    if (this.mResumedSession)
                    {
                        this.mSecurityParameters.masterSecret = Arrays.Clone(this.mSessionParameters.MasterSecret);
                        this.mRecordStream.SetPendingConnectionState(Peer.GetCompression(), Peer.GetCipher());
                    }
                    else
                    {
                        InvalidateSession();

                        if (this.mSelectedSessionID.Length > 0)
                        {
                            this.mTlsSession = new TlsSessionImpl(this.mSelectedSessionID, null);
                        }
                    }

                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }
                break;
            }
            case HandshakeType.supplemental_data:
            {
                switch (this.mConnectionState)
                {
                case CS_SERVER_HELLO:
                {
                    HandleSupplementalData(ReadSupplementalDataMessage(buf));
                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }
                break;
            }
            case HandshakeType.server_hello_done:
            {
                switch (this.mConnectionState)
                {
                case CS_SERVER_HELLO:
                case CS_SERVER_SUPPLEMENTAL_DATA:
                case CS_SERVER_CERTIFICATE:
                case CS_CERTIFICATE_STATUS:
                case CS_SERVER_KEY_EXCHANGE:
                case CS_CERTIFICATE_REQUEST:
                {
                    if (mConnectionState < CS_SERVER_SUPPLEMENTAL_DATA)
                    {
                        HandleSupplementalData(null);
                    }

                    if (mConnectionState < CS_SERVER_CERTIFICATE)
                    {
                        // There was no server certificate message; check it's OK
                        this.mKeyExchange.SkipServerCredentials();
                        this.mAuthentication = null;
                    }

                    if (mConnectionState < CS_SERVER_KEY_EXCHANGE)
                    {
                        // There was no server key exchange message; check it's OK
                        this.mKeyExchange.SkipServerKeyExchange();
                    }

                    AssertEmpty(buf);

                    this.mConnectionState = CS_SERVER_HELLO_DONE;

                    this.mRecordStream.HandshakeHash.SealHashAlgorithms();

                    IList clientSupplementalData = mTlsClient.GetClientSupplementalData();
                    if (clientSupplementalData != null)
                    {
                        SendSupplementalDataMessage(clientSupplementalData);
                    }
                    this.mConnectionState = CS_CLIENT_SUPPLEMENTAL_DATA;

                    TlsCredentials clientCreds = null;
                    if (mCertificateRequest == null)
                    {
                        this.mKeyExchange.SkipClientCredentials();
                    }
                    else
                    {
                        clientCreds = this.mAuthentication.GetClientCredentials(mCertificateRequest);

                        if (clientCreds == null)
                        {
                            this.mKeyExchange.SkipClientCredentials();

                            /*
                             * RFC 5246 If no suitable certificate is available, the client MUST Send a
                             * certificate message containing no certificates.
                             * 
                             * NOTE: In previous RFCs, this was SHOULD instead of MUST.
                             */
                            SendCertificateMessage(Certificate.EmptyChain);
                        }
                        else
                        {
                            this.mKeyExchange.ProcessClientCredentials(clientCreds);

                            SendCertificateMessage(clientCreds.Certificate);
                        }
                    }

                    this.mConnectionState = CS_CLIENT_CERTIFICATE;

                    /*
                     * Send the client key exchange message, depending on the key exchange we are using
                     * in our CipherSuite.
                     */
                    SendClientKeyExchangeMessage();
                    this.mConnectionState = CS_CLIENT_KEY_EXCHANGE;

                    if (TlsUtilities.IsSsl(Context))
                    {
                        EstablishMasterSecret(Context, mKeyExchange);
                    }

                    TlsHandshakeHash prepareFinishHash = mRecordStream.PrepareToFinish();
                    this.mSecurityParameters.sessionHash = GetCurrentPrfHash(Context, prepareFinishHash, null);

                    if (!TlsUtilities.IsSsl(Context))
                    {
                        EstablishMasterSecret(Context, mKeyExchange);
                    }

                    mRecordStream.SetPendingConnectionState(Peer.GetCompression(), Peer.GetCipher());

                    if (clientCreds != null && clientCreds is TlsSignerCredentials)
                    {
                        TlsSignerCredentials signerCredentials = (TlsSignerCredentials)clientCreds;

                        /*
                         * RFC 5246 4.7. digitally-signed element needs SignatureAndHashAlgorithm from TLS 1.2
                         */
                        SignatureAndHashAlgorithm signatureAndHashAlgorithm = TlsUtilities.GetSignatureAndHashAlgorithm(
                            Context, signerCredentials);

                        byte[] hash;
                        if (signatureAndHashAlgorithm == null)
                        {
                            hash = mSecurityParameters.SessionHash;
                        }
                        else
                        {
                            hash = prepareFinishHash.GetFinalHash(signatureAndHashAlgorithm.Hash);
                        }

                        byte[] signature = signerCredentials.GenerateCertificateSignature(hash);
                        DigitallySigned certificateVerify = new DigitallySigned(signatureAndHashAlgorithm, signature);
                        SendCertificateVerifyMessage(certificateVerify);

                        this.mConnectionState = CS_CERTIFICATE_VERIFY;
                    }

                    SendChangeCipherSpecMessage();
                    SendFinishedMessage();
                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                this.mConnectionState = CS_CLIENT_FINISHED;
                break;
            }
            case HandshakeType.server_key_exchange:
            {
                switch (this.mConnectionState)
                {
                case CS_SERVER_HELLO:
                case CS_SERVER_SUPPLEMENTAL_DATA:
                case CS_SERVER_CERTIFICATE:
                case CS_CERTIFICATE_STATUS:
                {
                    if (mConnectionState < CS_SERVER_SUPPLEMENTAL_DATA)
                    {
                        HandleSupplementalData(null);
                    }

                    if (mConnectionState < CS_SERVER_CERTIFICATE)
                    {
                        // There was no server certificate message; check it's OK
                        this.mKeyExchange.SkipServerCredentials();
                        this.mAuthentication = null;
                    }

                    this.mKeyExchange.ProcessServerKeyExchange(buf);

                    AssertEmpty(buf);
                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                this.mConnectionState = CS_SERVER_KEY_EXCHANGE;
                break;
            }
            case HandshakeType.certificate_request:
            {
                switch (this.mConnectionState)
                {
                case CS_SERVER_CERTIFICATE:
                case CS_CERTIFICATE_STATUS:
                case CS_SERVER_KEY_EXCHANGE:
                {
                    if (this.mConnectionState != CS_SERVER_KEY_EXCHANGE)
                    {
                        // There was no server key exchange message; check it's OK
                        this.mKeyExchange.SkipServerKeyExchange();
                    }

                    if (this.mAuthentication == null)
                    {
                        /*
                         * RFC 2246 7.4.4. It is a fatal handshake_failure alert for an anonymous server
                         * to request client identification.
                         */
                        throw new TlsFatalAlert(AlertDescription.handshake_failure);
                    }

                    this.mCertificateRequest = CertificateRequest.Parse(Context, buf);

                    AssertEmpty(buf);

                    this.mKeyExchange.ValidateCertificateRequest(this.mCertificateRequest);

                    /*
                     * TODO Give the client a chance to immediately select the CertificateVerify hash
                     * algorithm here to avoid tracking the other hash algorithms unnecessarily?
                     */
                    TlsUtilities.TrackHashAlgorithms(this.mRecordStream.HandshakeHash,
                        this.mCertificateRequest.SupportedSignatureAlgorithms);

                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                this.mConnectionState = CS_CERTIFICATE_REQUEST;
                break;
            }
            case HandshakeType.session_ticket:
            {
                switch (this.mConnectionState)
                {
                case CS_CLIENT_FINISHED:
                {
                    if (!this.mExpectSessionTicket)
                    {
                        /*
                         * RFC 5077 3.3. This message MUST NOT be sent if the server did not include a
                         * SessionTicket extension in the ServerHello.
                         */
                        throw new TlsFatalAlert(AlertDescription.unexpected_message);
                    }

                    /*
                     * RFC 5077 3.4. If the client receives a session ticket from the server, then it
                     * discards any Session ID that was sent in the ServerHello.
                     */
                    InvalidateSession();

                    ReceiveNewSessionTicketMessage(buf);
                    break;
                }
                default:
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                this.mConnectionState = CS_SERVER_SESSION_TICKET;
                break;
            }
            case HandshakeType.hello_request:
            {
                AssertEmpty(buf);

                /*
                 * RFC 2246 7.4.1.1 Hello request This message will be ignored by the client if the
                 * client is currently negotiating a session. This message may be ignored by the client
                 * if it does not wish to renegotiate a session, or the client may, if it wishes,
                 * respond with a no_renegotiation alert.
                 */
                if (this.mConnectionState == CS_END)
                {
                    RefuseRenegotiation();
                }
                break;
            }
            case HandshakeType.client_hello:
            case HandshakeType.client_key_exchange:
            case HandshakeType.certificate_verify:
            case HandshakeType.hello_verify_request:
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }
        }

        protected virtual void HandleSupplementalData(IList serverSupplementalData)
        {
            this.mTlsClient.ProcessServerSupplementalData(serverSupplementalData);
            this.mConnectionState = CS_SERVER_SUPPLEMENTAL_DATA;

            this.mKeyExchange = mTlsClient.GetKeyExchange();
            this.mKeyExchange.Init(Context);
        }

        protected virtual void ReceiveNewSessionTicketMessage(MemoryStream buf)
        {
            NewSessionTicket newSessionTicket = NewSessionTicket.Parse(buf);

            AssertEmpty(buf);

            mTlsClient.NotifyNewSessionTicket(newSessionTicket);
        }

        protected virtual void ReceiveServerHelloMessage(MemoryStream buf)
        {
            {
                ProtocolVersion server_version = TlsUtilities.ReadVersion(buf);
                if (server_version.IsDtls)
                    throw new TlsFatalAlert(AlertDescription.illegal_parameter);

                // Check that this matches what the server is Sending in the record layer
                if (!server_version.Equals(this.mRecordStream.ReadVersion))
                    throw new TlsFatalAlert(AlertDescription.illegal_parameter);

                ProtocolVersion client_version = Context.ClientVersion;
                if (!server_version.IsEqualOrEarlierVersionOf(client_version))
                    throw new TlsFatalAlert(AlertDescription.illegal_parameter);

                this.mRecordStream.SetWriteVersion(server_version);
                ContextAdmin.SetServerVersion(server_version);
                this.mTlsClient.NotifyServerVersion(server_version);
            }

            /*
             * Read the server random
             */
            this.mSecurityParameters.serverRandom = TlsUtilities.ReadFully(32, buf);

            this.mSelectedSessionID = TlsUtilities.ReadOpaque8(buf);
            if (this.mSelectedSessionID.Length > 32)
                throw new TlsFatalAlert(AlertDescription.illegal_parameter);
            this.mTlsClient.NotifySessionID(this.mSelectedSessionID);
            this.mResumedSession = this.mSelectedSessionID.Length > 0 && this.mTlsSession != null
                && Arrays.AreEqual(this.mSelectedSessionID, this.mTlsSession.SessionID);

            /*
             * Find out which CipherSuite the server has chosen and check that it was one of the offered
             * ones, and is a valid selection for the negotiated version.
             */
            int selectedCipherSuite = TlsUtilities.ReadUint16(buf);
            if (!Arrays.Contains(this.mOfferedCipherSuites, selectedCipherSuite)
                || selectedCipherSuite == CipherSuite.TLS_NULL_WITH_NULL_NULL
                || CipherSuite.IsScsv(selectedCipherSuite)
                || !TlsUtilities.IsValidCipherSuiteForVersion(selectedCipherSuite, Context.ServerVersion))
            {
                throw new TlsFatalAlert(AlertDescription.illegal_parameter);
            }
            this.mTlsClient.NotifySelectedCipherSuite(selectedCipherSuite);

            /*
             * Find out which CompressionMethod the server has chosen and check that it was one of the
             * offered ones.
             */
            byte selectedCompressionMethod = TlsUtilities.ReadUint8(buf);
            if (!Arrays.Contains(this.mOfferedCompressionMethods, selectedCompressionMethod))
                throw new TlsFatalAlert(AlertDescription.illegal_parameter);
            this.mTlsClient.NotifySelectedCompressionMethod(selectedCompressionMethod);

            /*
             * RFC 3546 2.2 The extended server hello message format MAY be sent in place of the server
             * hello message when the client has requested extended functionality via the extended
             * client hello message specified in Section 2.1. ... Note that the extended server hello
             * message is only sent in response to an extended client hello message. This prevents the
             * possibility that the extended server hello message could "break" existing TLS 1.0
             * clients.
             */
            this.mServerExtensions = ReadExtensions(buf);

            /*
             * RFC 7627 4. Clients and servers SHOULD NOT accept handshakes that do not use the extended
             * master secret [..]. (and see 5.2, 5.3)
             */
            this.mSecurityParameters.extendedMasterSecret = !TlsUtilities.IsSsl(mTlsClientContext)
                && TlsExtensionsUtilities.HasExtendedMasterSecretExtension(mServerExtensions);

            if (!mSecurityParameters.IsExtendedMasterSecret
                && (mResumedSession || mTlsClient.RequiresExtendedMasterSecret()))
            {
                throw new TlsFatalAlert(AlertDescription.handshake_failure);
            }

            /*
             * RFC 3546 2.2 Note that the extended server hello message is only sent in response to an
             * extended client hello message.
             * 
             * However, see RFC 5746 exception below. We always include the SCSV, so an Extended Server
             * Hello is always allowed.
             */
            if (this.mServerExtensions != null)
            {
                foreach (int extType in this.mServerExtensions.Keys)
                {
                    /*
                     * RFC 5746 3.6. Note that Sending a "renegotiation_info" extension in response to a
                     * ClientHello containing only the SCSV is an explicit exception to the prohibition
                     * in RFC 5246, Section 7.4.1.4, on the server Sending unsolicited extensions and is
                     * only allowed because the client is signaling its willingness to receive the
                     * extension via the TLS_EMPTY_RENEGOTIATION_INFO_SCSV SCSV.
                     */
                    if (extType == ExtensionType.renegotiation_info)
                        continue;

                    /*
                     * RFC 5246 7.4.1.4 An extension type MUST NOT appear in the ServerHello unless the
                     * same extension type appeared in the corresponding ClientHello. If a client
                     * receives an extension type in ServerHello that it did not request in the
                     * associated ClientHello, it MUST abort the handshake with an unsupported_extension
                     * fatal alert.
                     */
                    if (null == TlsUtilities.GetExtensionData(this.mClientExtensions, extType))
                        throw new TlsFatalAlert(AlertDescription.unsupported_extension);

                    /*
                     * RFC 3546 2.3. If [...] the older session is resumed, then the server MUST ignore
                     * extensions appearing in the client hello, and Send a server hello containing no
                     * extensions[.]
                     */
                    if (this.mResumedSession)
                    {
                        // TODO[compat-gnutls] GnuTLS test server Sends server extensions e.g. ec_point_formats
                        // TODO[compat-openssl] OpenSSL test server Sends server extensions e.g. ec_point_formats
                        // TODO[compat-polarssl] PolarSSL test server Sends server extensions e.g. ec_point_formats
    //                    throw new TlsFatalAlert(AlertDescription.illegal_parameter);
                    }
                }
            }

            /*
             * RFC 5746 3.4. Client Behavior: Initial Handshake
             */
            {
                /*
                 * When a ServerHello is received, the client MUST check if it includes the
                 * "renegotiation_info" extension:
                 */
                byte[] renegExtData = TlsUtilities.GetExtensionData(this.mServerExtensions, ExtensionType.renegotiation_info);
                if (renegExtData != null)
                {
                    /*
                     * If the extension is present, set the secure_renegotiation flag to TRUE. The
                     * client MUST then verify that the length of the "renegotiated_connection"
                     * field is zero, and if it is not, MUST abort the handshake (by Sending a fatal
                     * handshake_failure alert).
                     */
                    this.mSecureRenegotiation = true;

                    if (!Arrays.ConstantTimeAreEqual(renegExtData, CreateRenegotiationInfo(TlsUtilities.EmptyBytes)))
                        throw new TlsFatalAlert(AlertDescription.handshake_failure);
                }
            }

            // TODO[compat-gnutls] GnuTLS test server fails to Send renegotiation_info extension when resuming
            this.mTlsClient.NotifySecureRenegotiation(this.mSecureRenegotiation);

            IDictionary sessionClientExtensions = mClientExtensions, sessionServerExtensions = mServerExtensions;
            if (this.mResumedSession)
            {
                if (selectedCipherSuite != this.mSessionParameters.CipherSuite
                    || selectedCompressionMethod != this.mSessionParameters.CompressionAlgorithm)
                {
                    throw new TlsFatalAlert(AlertDescription.illegal_parameter);
                }

                sessionClientExtensions = null;
                sessionServerExtensions = this.mSessionParameters.ReadServerExtensions();
            }

            this.mSecurityParameters.cipherSuite = selectedCipherSuite;
            this.mSecurityParameters.compressionAlgorithm = selectedCompressionMethod;

            if (sessionServerExtensions != null && sessionServerExtensions.Count > 0)
            {
                {
                    /*
                     * RFC 7366 3. If a server receives an encrypt-then-MAC request extension from a client
                     * and then selects a stream or Authenticated Encryption with Associated Data (AEAD)
                     * ciphersuite, it MUST NOT send an encrypt-then-MAC response extension back to the
                     * client.
                     */
                    bool serverSentEncryptThenMAC = TlsExtensionsUtilities.HasEncryptThenMacExtension(sessionServerExtensions);
                    if (serverSentEncryptThenMAC && !TlsUtilities.IsBlockCipherSuite(selectedCipherSuite))
                        throw new TlsFatalAlert(AlertDescription.illegal_parameter);

                    this.mSecurityParameters.encryptThenMac = serverSentEncryptThenMAC;
                }

                this.mSecurityParameters.maxFragmentLength = ProcessMaxFragmentLengthExtension(sessionClientExtensions,
                    sessionServerExtensions, AlertDescription.illegal_parameter);

                this.mSecurityParameters.truncatedHMac = TlsExtensionsUtilities.HasTruncatedHMacExtension(sessionServerExtensions);

                /*
                 * TODO It's surprising that there's no provision to allow a 'fresh' CertificateStatus to be sent in
                 * a session resumption handshake.
                 */
                this.mAllowCertificateStatus = !this.mResumedSession
                    && TlsUtilities.HasExpectedEmptyExtensionData(sessionServerExtensions, ExtensionType.status_request,
                        AlertDescription.illegal_parameter);

                this.mExpectSessionTicket = !this.mResumedSession
                    && TlsUtilities.HasExpectedEmptyExtensionData(sessionServerExtensions, ExtensionType.session_ticket,
                        AlertDescription.illegal_parameter);
            }

            if (sessionClientExtensions != null)
            {
                this.mTlsClient.ProcessServerExtensions(sessionServerExtensions);
            }

            this.mSecurityParameters.prfAlgorithm = GetPrfAlgorithm(Context, this.mSecurityParameters.CipherSuite);

            /*
             * RFC 5246 7.4.9. Any cipher suite which does not explicitly specify
             * verify_data_length has a verify_data_length equal to 12. This includes all
             * existing cipher suites.
             */
            this.mSecurityParameters.verifyDataLength = 12;
        }

        protected virtual void SendCertificateVerifyMessage(DigitallySigned certificateVerify)
        {
            HandshakeMessage message = new HandshakeMessage(HandshakeType.certificate_verify);

            certificateVerify.Encode(message);

            message.WriteToRecordStream(this);
        }

        protected virtual void SendClientHelloMessage()
        {
            this.mRecordStream.SetWriteVersion(this.mTlsClient.ClientHelloRecordLayerVersion);

            ProtocolVersion client_version = this.mTlsClient.ClientVersion;
            if (client_version.IsDtls)
                throw new TlsFatalAlert(AlertDescription.internal_error);

            ContextAdmin.SetClientVersion(client_version);

            /*
             * TODO RFC 5077 3.4. When presenting a ticket, the client MAY generate and include a
             * Session ID in the TLS ClientHello.
             */
            byte[] session_id = TlsUtilities.EmptyBytes;
            if (this.mTlsSession != null)
            {
                session_id = this.mTlsSession.SessionID;
                if (session_id == null || session_id.Length > 32)
                {
                    session_id = TlsUtilities.EmptyBytes;
                }
            }

            bool fallback = this.mTlsClient.IsFallback;

            this.mOfferedCipherSuites = this.mTlsClient.GetCipherSuites();

            this.mOfferedCompressionMethods = this.mTlsClient.GetCompressionMethods();

            if (session_id.Length > 0 && this.mSessionParameters != null)
            {
                if (!mSessionParameters.IsExtendedMasterSecret
                    || !Arrays.Contains(this.mOfferedCipherSuites, mSessionParameters.CipherSuite)
                    || !Arrays.Contains(this.mOfferedCompressionMethods, mSessionParameters.CompressionAlgorithm))
                {
                    session_id = TlsUtilities.EmptyBytes;
                }
            }

            this.mClientExtensions = TlsExtensionsUtilities.EnsureExtensionsInitialised(this.mTlsClient.GetClientExtensions());

            if (!client_version.IsSsl)
            {
                TlsExtensionsUtilities.AddExtendedMasterSecretExtension(this.mClientExtensions);
            }

            HandshakeMessage message = new HandshakeMessage(HandshakeType.client_hello);

            TlsUtilities.WriteVersion(client_version, message);

            message.Write(this.mSecurityParameters.ClientRandom);

            TlsUtilities.WriteOpaque8(session_id, message);

            // Cipher Suites (and SCSV)
            {
                /*
                 * RFC 5746 3.4. The client MUST include either an empty "renegotiation_info" extension,
                 * or the TLS_EMPTY_RENEGOTIATION_INFO_SCSV signaling cipher suite value in the
                 * ClientHello. Including both is NOT RECOMMENDED.
                 */
                byte[] renegExtData = TlsUtilities.GetExtensionData(mClientExtensions, ExtensionType.renegotiation_info);
                bool noRenegExt = (null == renegExtData);

                bool noRenegScsv = !Arrays.Contains(mOfferedCipherSuites, CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV);

                if (noRenegExt && noRenegScsv)
                {
                    // TODO Consider whether to default to a client extension instead
                    this.mOfferedCipherSuites = Arrays.Append(mOfferedCipherSuites, CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV);
                }

                /*
                 * RFC 7507 4. If a client sends a ClientHello.client_version containing a lower value
                 * than the latest (highest-valued) version supported by the client, it SHOULD include
                 * the TLS_FALLBACK_SCSV cipher suite value in ClientHello.cipher_suites [..]. (The
                 * client SHOULD put TLS_FALLBACK_SCSV after all cipher suites that it actually intends
                 * to negotiate.)
                 */
                if (fallback && !Arrays.Contains(mOfferedCipherSuites, CipherSuite.TLS_FALLBACK_SCSV))
                {
                    this.mOfferedCipherSuites = Arrays.Append(mOfferedCipherSuites, CipherSuite.TLS_FALLBACK_SCSV);
                }

                TlsUtilities.WriteUint16ArrayWithUint16Length(mOfferedCipherSuites, message);
            }

            TlsUtilities.WriteUint8ArrayWithUint8Length(mOfferedCompressionMethods, message);

            WriteExtensions(message, mClientExtensions);

            message.WriteToRecordStream(this);
        }

        protected virtual void SendClientKeyExchangeMessage()
        {
            HandshakeMessage message = new HandshakeMessage(HandshakeType.client_key_exchange);

            this.mKeyExchange.GenerateClientKeyExchange(message);

            message.WriteToRecordStream(this);
        }
    }
}
