using System;
using System.IO;

namespace Unosquare.Swan.Networking.Ldap {
  /// <summary>
  /// Represents an Ldap Message.
  /// <pre>
  /// LdapMessage ::= SEQUENCE {
  /// messageID       MessageID,
  /// protocolOp      CHOICE {
  /// bindRequest     BindRequest,
  /// bindResponse    BindResponse,
  /// unbindRequest   UnbindRequest,
  /// searchRequest   SearchRequest,
  /// searchResEntry  SearchResultEntry,
  /// searchResDone   SearchResultDone,
  /// searchResRef    SearchResultReference,
  /// modifyRequest   ModifyRequest,
  /// modifyResponse  ModifyResponse,
  /// addRequest      AddRequest,
  /// addResponse     AddResponse,
  /// delRequest      DelRequest,
  /// delResponse     DelResponse,
  /// modDNRequest    ModifyDNRequest,
  /// modDNResponse   ModifyDNResponse,
  /// compareRequest  CompareRequest,
  /// compareResponse CompareResponse,
  /// abandonRequest  AbandonRequest,
  /// extendedReq     ExtendedRequest,
  /// extendedResp    ExtendedResponse },
  /// controls       [0] Controls OPTIONAL }
  /// </pre>
  /// Note: The creation of a MessageID should be hidden within the creation of
  /// an RfcLdapMessage. The MessageID needs to be in sequence, and has an
  /// upper and lower limit. There is never a case when a user should be
  /// able to specify the MessageID for an RfcLdapMessage. The MessageID()
  /// constructor should be package protected. (So the MessageID value
  /// isn't arbitrarily run up.).
  /// </summary>
  /// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
  internal sealed class RfcLdapMessage : Asn1Sequence {
    private readonly Asn1Object _op;

    /// <summary>
    /// Initializes a new instance of the <see cref="RfcLdapMessage"/> class.
    /// Create an RfcLdapMessage request from input parameters.
    /// </summary>
    /// <param name="op">The op.</param>
    /// <param name="controls">The controls.</param>
    public RfcLdapMessage(IRfcRequest op, RfcControls controls)
        : base(3) {
      this._op = (Asn1Object)op;

      this.Add(new RfcMessageID()); // MessageID has static counter
      this.Add((Asn1Object)op);
      if(controls != null) {
        this.Add(controls);
      }
    }


    /// <summary>
    /// Initializes a new instance of the <see cref="RfcLdapMessage"/> class.
    /// Will decode an RfcLdapMessage directly from an InputStream.
    /// </summary>
    /// <param name="stream">The stream.</param>
    /// <param name="len">The length.</param>
    /// <exception cref="Exception">RfcLdapMessage: Invalid tag: " + protocolOpId.Tag.</exception>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "<Ausstehend>")]
    public RfcLdapMessage(Stream stream, Int32 len)
        : base(stream, len) {
      // Decode implicitly tagged protocol operation from an Asn1Tagged type
      // to its appropriate application type.
      Asn1Tagged protocolOp = (Asn1Tagged)this.Get(1);
      Asn1Identifier protocolOpId = protocolOp.GetIdentifier();
      SByte[] content = ((Asn1OctetString)protocolOp.TaggedValue).ByteValue();
      MemoryStream bais = new MemoryStream(content.ToByteArray());

      switch((LdapOperation)protocolOpId.Tag) {
        case LdapOperation.SearchResponse:
          this.Set(1, new RfcSearchResultEntry(bais, content.Length));
          break;

        case LdapOperation.SearchResult:
          this.Set(1, new RfcSearchResultDone(bais, content.Length));
          break;

        case LdapOperation.SearchResultReference:
          this.Set(1, new RfcSearchResultReference(bais, content.Length));
          break;

        case LdapOperation.BindResponse:
          this.Set(1, new RfcBindResponse(bais, content.Length));
          break;

        case LdapOperation.ExtendedResponse:
          this.Set(1, new RfcExtendedResponse(bais, content.Length));
          break;

        case LdapOperation.IntermediateResponse:
          this.Set(1, new RfcIntermediateResponse(bais, content.Length));
          break;
        case LdapOperation.ModifyResponse:
          this.Set(1, new RfcModifyResponse(bais, content.Length));
          break;

        default:
          throw new InvalidOperationException($"RfcLdapMessage: Invalid tag: {protocolOpId.Tag}");
      }

      // decode optional implicitly tagged controls from Asn1Tagged type to
      // to RFC 2251 types.
      if(this.Size() <= 2) {
        return;
      }

      Asn1Tagged controls = (Asn1Tagged)this.Get(2);
      content = ((Asn1OctetString)controls.TaggedValue).ByteValue();

      using(MemoryStream ms = new MemoryStream(content.ToByteArray())) {
        this.Set(2, new RfcControls(ms, content.Length));
      }
    }

    public Int32 MessageId => ((Asn1Integer)this.Get(0)).IntValue();

    /// <summary> Returns this RfcLdapMessage's message type.</summary>
    public LdapOperation Type => (LdapOperation)this.Get(1).GetIdentifier().Tag;

    public Asn1Object Response => this.Get(1);

    public String RequestDn => ((IRfcRequest)this._op).GetRequestDN();

    public LdapMessage RequestingMessage {
      get; set;
    }

    public IRfcRequest GetRequest() => (IRfcRequest)this.Get(1);

    public Boolean IsRequest() => this.Get(1) is IRfcRequest;
  }

  /// <summary>
  /// Represents Ldap Controls.
  /// <pre>
  /// Controls ::= SEQUENCE OF Control
  /// </pre>
  /// </summary>
  /// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1SequenceOf" />
  internal class RfcControls : Asn1SequenceOf {
    public const Int32 Controls = 0;

    public RfcControls()
        : base(5) {
    }

    public RfcControls(Stream stream, Int32 len)
        : base(stream, len) {
      // Convert each SEQUENCE element to a Control
      for(Int32 i = 0; i < this.Size(); i++) {
        RfcControl tempControl = new RfcControl((Asn1Sequence)this.Get(i));
        this.Set(i, tempControl);
      }
    }

    public void Add(RfcControl control) => base.Add(control);

    public void Set(Int32 index, RfcControl control) => base.Set(index, control);

    public override Asn1Identifier GetIdentifier() => new Asn1Identifier(Controls, true);
  }

  /// <summary>
  /// This interface represents RfcLdapMessages that contain a response from a
  /// server.
  /// If the protocol operation of the RfcLdapMessage is of this type,
  /// it contains at least an RfcLdapResult.
  /// </summary>
  internal interface IRfcResponse {
    /// <summary>
    /// Gets the result code.
    /// </summary>
    /// <returns>Asn1Enumerated.</returns>
    Asn1Enumerated GetResultCode();

    /// <summary>
    /// Gets the matched dn.
    /// </summary>
    /// <returns>RfcLdapDN.</returns>
    Asn1OctetString GetMatchedDN();

    /// <summary>
    /// Gets the error message.
    /// </summary>
    /// <returns>RfcLdapString.</returns>
    Asn1OctetString GetErrorMessage();

    /// <summary>
    /// Gets the referral.
    /// </summary>
    /// <returns>Asn1SequenceOf.</returns>
    Asn1SequenceOf GetReferral();
  }

  /// <summary>
  /// This interface represents Protocol Operations that are requests from a
  /// client.
  /// </summary>
  internal interface IRfcRequest {
    /// <summary>
    /// Builds a new request using the data from the this object.
    /// </summary>
    /// <returns>A <see cref="System.String" />.</returns>
    String GetRequestDN();
  }

  /// <summary>
  /// Represents an LdapResult.
  /// <pre>
  /// LdapResult ::= SEQUENCE {
  /// resultCode      ENUMERATED {
  /// success                      (0),
  /// operationsError              (1),
  /// protocolError                (2),
  /// timeLimitExceeded            (3),
  /// sizeLimitExceeded            (4),
  /// compareFalse                 (5),
  /// compareTrue                  (6),
  /// authMethodNotSupported       (7),
  /// strongAuthRequired           (8),
  /// -- 9 reserved --
  /// referral                     (10),  -- new
  /// adminLimitExceeded           (11),  -- new
  /// unavailableCriticalExtension (12),  -- new
  /// confidentialityRequired      (13),  -- new
  /// saslBindInProgress           (14),  -- new
  /// noSuchAttribute              (16),
  /// undefinedAttributeType       (17),
  /// inappropriateMatching        (18),
  /// constraintViolation          (19),
  /// attributeOrValueExists       (20),
  /// invalidAttributeSyntax       (21),
  /// -- 22-31 unused --
  /// noSuchObject                 (32),
  /// aliasProblem                 (33),
  /// invalidDNSyntax              (34),
  /// -- 35 reserved for undefined isLeaf --
  /// aliasDereferencingProblem    (36),
  /// -- 37-47 unused --
  /// inappropriateAuthentication  (48),
  /// invalidCredentials           (49),
  /// insufficientAccessRights     (50),
  /// busy                         (51),
  /// unavailable                  (52),
  /// unwillingToPerform           (53),
  /// loopDetect                   (54),
  /// -- 55-63 unused --
  /// namingViolation              (64),
  /// objectClassViolation         (65),
  /// notAllowedOnNonLeaf          (66),
  /// notAllowedOnRDN              (67),
  /// entryAlreadyExists           (68),
  /// objectClassModsProhibited    (69),
  /// -- 70 reserved for CLdap --
  /// affectsMultipleDSAs          (71), -- new
  /// -- 72-79 unused --
  /// other                        (80) },
  /// -- 81-90 reserved for APIs --
  /// matchedDN       LdapDN,
  /// errorMessage    LdapString,
  /// referral        [3] Referral OPTIONAL }
  /// </pre>
  /// </summary>
  /// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
  /// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
  internal class RfcLdapResult : Asn1Sequence, IRfcResponse {
    public const Int32 Referral = 3;

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0068:Empfohlenes Dispose-Muster verwenden", Justification = "<Ausstehend>")]
    public RfcLdapResult(Stream stream, Int32 len)
        : base(stream, len) {
      // Decode optional referral from Asn1OctetString to Referral.
      if(this.Size() <= 3) {
        return;
      }

      Asn1Tagged obj = (Asn1Tagged)this.Get(3);
      Asn1Identifier id = obj.GetIdentifier();

      if(id.Tag != Referral) {
        return;
      }

      SByte[] content = ((Asn1OctetString)obj.TaggedValue).ByteValue();
      this.Set(3, new Asn1SequenceOf(new MemoryStream(content.ToByteArray()), content.Length));
    }

    public Asn1Enumerated GetResultCode() => (Asn1Enumerated)this.Get(0);

    public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString)this.Get(1)).ByteValue());

    public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString)this.Get(2)).ByteValue());

    public Asn1SequenceOf GetReferral() => this.Size() > 3 ? (Asn1SequenceOf)this.Get(3) : null;
  }

  /// <summary>
  /// Represents an Ldap Search Result Done Response.
  /// <pre>
  /// SearchResultDone ::= [APPLICATION 5] LdapResult
  /// </pre>
  /// </summary>
  /// <seealso cref="Unosquare.Swan.Networking.Ldap.RfcLdapResult" />
  internal class RfcSearchResultDone : RfcLdapResult {
    public RfcSearchResultDone(Stream stream, Int32 len)
        : base(stream, len) {
    }

    public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResult);
  }

  /// <summary>
  /// Represents an Ldap Search Result Entry.
  /// <pre>
  /// SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
  /// objectName      LdapDN,
  /// attributes      PartialAttributeList }
  /// </pre>
  /// </summary>
  /// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
  internal sealed class RfcSearchResultEntry : Asn1Sequence {
    public RfcSearchResultEntry(Stream stream, Int32 len)
        : base(stream, len) {
    }

    public String ObjectName => ((Asn1OctetString)this.Get(0)).StringValue();

    public Asn1Sequence Attributes => (Asn1Sequence)this.Get(1);

    public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResponse);
  }

  /// <summary>
  /// Represents an Ldap Message ID.
  /// <pre>
  /// MessageID ::= INTEGER (0 .. maxInt)
  /// maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --
  /// Note: The creation of a MessageID should be hidden within the creation of
  /// an RfcLdapMessage. The MessageID needs to be in sequence, and has an
  /// upper and lower limit. There is never a case when a user should be
  /// able to specify the MessageID for an RfcLdapMessage. The MessageID()
  /// class should be package protected. (So the MessageID value isn't
  /// arbitrarily run up.)
  /// </pre></summary>
  /// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Integer" />
  internal class RfcMessageID : Asn1Integer {
    private static Int32 _messageId;
    private static readonly Object SyncRoot = new Object();

    /// <summary>
    /// Initializes a new instance of the <see cref="RfcMessageID"/> class.
    /// Creates a MessageID with an auto incremented Asn1Integer value.
    /// Bounds: (0 .. 2,147,483,647) (2^^31 - 1 or Integer.MAX_VALUE)
    /// MessageID zero is never used in this implementation.  Always
    /// start the messages with one.
    /// </summary>
    protected internal RfcMessageID()
        : base(MessageId) {
    }

    private static Int32 MessageId {
      get {
        lock(SyncRoot) {
          return _messageId < Int32.MaxValue ? ++_messageId : (_messageId = 1);
        }
      }
    }
  }
}