using Disco.Data.Repository;
using Disco.Models.Repository;
using Disco.Services.Interop.ActiveDirectory;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Text;

namespace Disco.Services.Documents
{
    public class DocumentUniqueIdentifier
    {
        private const int CurrentVersion = 2;
        private const byte MagicNumber = 0xC4;
        private DiscoDataContext database;

        public int Version { get; private set; }
        public short DeploymentChecksum { get; private set; }
        public string DocumentTemplateId { get; private set; }
        public string TargetId { get; private set; }
        public string CreatorId { get; private set; }
        public DateTime TimeStamp { get; private set; }
        public int PageIndex { get; private set; }

        private DocumentTemplate documentTemplate;
        private AttachmentTypes? attachmentType;
        private IAttachmentTarget target;
        private User creator;

        public DocumentTemplate DocumentTemplate
        {
            get
            {
                if (documentTemplate == null && !string.IsNullOrWhiteSpace(DocumentTemplateId) && !DocumentTemplateId.StartsWith("--", StringComparison.Ordinal))
                {
                    documentTemplate = database.DocumentTemplates.Find(DocumentTemplateId);
                    if (documentTemplate != null)
                    {
                        attachmentType = documentTemplate.AttachmentType;
                        DocumentTemplateId = documentTemplate.Id;
                    }
                }

                return documentTemplate;
            }
        }

        public string DocumentGroupingId
        {
            get
            {
                // Unique identifier to distinguish this document from others (but keep pages together)
                return $"{TimeStamp.Ticks}|{TargetId}|{DocumentTemplateId}|{CreatorId}|{Version}|{DeploymentChecksum}";
            }
        }

        public AttachmentTypes? AttachmentType
        {
            get
            {
                if (!attachmentType.HasValue)
                {
                    if (DocumentTemplateId.StartsWith("--", StringComparison.Ordinal))
                    {
                        if (DocumentTemplateId.Equals("--DEVICE", StringComparison.Ordinal))
                        {
                            attachmentType = AttachmentTypes.Device;
                        }
                        else if (DocumentTemplateId.Equals("--JOB", StringComparison.Ordinal))
                        {
                            attachmentType = AttachmentTypes.Job;
                        }
                        else if (DocumentTemplateId.Equals("--USER", StringComparison.Ordinal))
                        {
                            attachmentType = AttachmentTypes.User;
                        }
                    }
                    else
                    {
                        var dt = DocumentTemplate;
                        if (dt != null)
                        {
                            attachmentType = dt.AttachmentType;
                        }
                    }
                }

                return attachmentType;
            }
        }

        public IAttachmentTarget Target
        {
            get
            {
                if (target == null)
                {
                    switch (AttachmentType)
                    {
                        case AttachmentTypes.Device:
                            target = database.Devices.Find(TargetId);
                            break;
                        case AttachmentTypes.Job:
                            target = database.Jobs.Find(int.Parse(TargetId));
                            break;
                        case AttachmentTypes.User:
                            target = database.Users.Find(ActiveDirectory.ParseDomainAccountId(TargetId));
                            break;
                        default:
                            throw new ArgumentException("Unexpected Attachment Type", nameof(AttachmentType));
                    }
                    if (target != null)
                    {
                        TargetId = target.AttachmentReferenceId;
                    }
                }

                return target;
            }
        }

        public User Creator
        {
            get
            {
                if (creator == null)
                {
                    creator = database.Users.Find(ActiveDirectory.ParseDomainAccountId(CreatorId));
                    if (creator != null)
                    {
                        CreatorId = creator.UserId;
                    }
                }
                return creator;
            }
        }

        private DocumentUniqueIdentifier(DiscoDataContext Database, int Version, short DeploymentChecksum, string DocumentTemplateId, string TargetId, string CreatorId, DateTime TimeStamp, int PageIndex, AttachmentTypes? AttachmentType)
        {
            database = Database;
            this.Version = Version;
            this.DeploymentChecksum = DeploymentChecksum;
            this.DocumentTemplateId = DocumentTemplateId;
            attachmentType = AttachmentType;
            this.TargetId = TargetId;
            this.CreatorId = ActiveDirectory.ParseDomainAccountId(CreatorId);
            this.TimeStamp = TimeStamp;
            this.PageIndex = PageIndex;
        }

        public string ToQRCodeString()
        {
            return $"Disco|1|{DocumentTemplate.Id}|{TargetId}|{CreatorId}|{TimeStamp:s}|{PageIndex}";
        }

        public string ToJson()
        {
            var builder = new StringBuilder();
            using (var stringWriter = new StringWriter(builder))
            {
                using (var writer = new JsonTextWriter(stringWriter))
                {
                    writer.Formatting = Formatting.Indented;
                    writer.WriteStartObject();
                    writer.WritePropertyName("version");
                    writer.WriteValue(CurrentVersion);
                    writer.WritePropertyName("attachmentType");
                    writer.WriteValue(AttachmentType.Value.ToString());
                    writer.WritePropertyName("documentTemplateId");
                    writer.WriteValue(DocumentTemplateId);
                    writer.WritePropertyName("targetId");
                    writer.WriteValue(TargetId);
                    writer.WritePropertyName("creatorId");
                    writer.WriteValue(CreatorId);
                    writer.WritePropertyName("timestamp");
                    writer.WriteValue(TimeStamp);
                    writer.WritePropertyName("pageIndex");
                    writer.WriteValue(PageIndex);
                    writer.WriteEndObject();
                }
            }
            return builder.ToString();
        }

        public byte[] ToQRCodeBytes()
        {
            // Byte | Meaning
            // 0    | magic number = 0xC4
            // 1    | bits 0-3 = version;
            //      |      4   = flag: has document template id
            //      |      5-6 = (01 = device attachment, 10 = job attachment, 11 = user attachment)
            //      |      7   = not used
            // 2-3  | deployment checksum (int16)
            // 4-7  | timestamp (uint32 unix epoch)
            // 8-9  | page index (uint16)
            // 10   | creator id encoded
            // ?    | target id encoded
            // ?    | document template id encoded (optional based on flag)
            const int version = 2;

            // encode variable-length parameters first

            // encode creator id (strip default domain)
            var creatorIdBytes = this.BinaryEncode(ActiveDirectory.FriendlyAccountId(CreatorId));

            // encode target id
            byte[] targetIdBytes;
            if (AttachmentType.HasValue && AttachmentType.Value == AttachmentTypes.User)
            {
                // strip default domain from user targetted attachments
                targetIdBytes = this.BinaryEncode(ActiveDirectory.FriendlyAccountId(TargetId));
            }
            else
            {
                targetIdBytes = this.BinaryEncode(TargetId);
            }

            byte[] documentTemplateIdBytes = null;
            if (DocumentTemplateId != null)
            {
                documentTemplateIdBytes = this.BinaryEncode(DocumentTemplateId);
            }

            var result = new byte[10 + creatorIdBytes.Length + targetIdBytes.Length + (documentTemplateIdBytes?.Length ?? 0)];

            // write magic number
            result[0] = MagicNumber;

            // write version
            result[1] = (version << 4);

            // write 'has document template id' flag
            if (documentTemplateIdBytes != null)
            {
                result[1] |= 0x8; // 0000 1000
            }

            // write attachment type
            switch (AttachmentType)
            {
                case AttachmentTypes.Device:
                    result[1] |= 0x2; // 0000 0010 - 01
                    break;
                case AttachmentTypes.Job:
                    result[1] |= 0x4; // 0000 0100 - 10
                    break;
                case AttachmentTypes.User:
                    result[1] |= 0x6; // 0000 0110 - 11
                    break;
            }

            // write deployment checksum
            result[2] = (byte)(DeploymentChecksum >> 8);
            result[3] = (byte)DeploymentChecksum;

            // write timestamp
            var timestamp = (uint)(TimeStamp.ToUniversalTime().Subtract(DateTime.FromFileTimeUtc(116444736000000000L)).Ticks / TimeSpan.TicksPerSecond);
            result[4] = (byte)(timestamp >> 24);
            result[5] = (byte)(timestamp >> 16);
            result[6] = (byte)(timestamp >> 8);
            result[7] = (byte)timestamp;

            // write page index
            result[8] = (byte)(PageIndex >> 8);
            result[9] = (byte)PageIndex;

            // write creator id
            creatorIdBytes.CopyTo(result, 10);

            // write target id
            targetIdBytes.CopyTo(result, 10 + creatorIdBytes.Length);

            // write document template id
            if (documentTemplateIdBytes != null)
            {
                documentTemplateIdBytes.CopyTo(result, 10 + creatorIdBytes.Length + targetIdBytes.Length);
            }

            return result;
        }

        public static DocumentUniqueIdentifier Create(DiscoDataContext Database, DocumentTemplate DocumentTemplate, IAttachmentTarget Target, User Creator, DateTime TimeStamp, int PageIndex)
        {
            var deploymentChecksum = Database.DiscoConfiguration.DeploymentChecksum;
            var documentTemplateId = DocumentTemplate.Id;
            var targetId = Target.AttachmentReferenceId;
            var creatorId = Creator.UserId;
            var attachmentType = DocumentTemplate.AttachmentType;

            var identifier = new DocumentUniqueIdentifier(Database, CurrentVersion, deploymentChecksum, documentTemplateId, targetId, creatorId, TimeStamp, PageIndex, attachmentType);

            identifier.documentTemplate = DocumentTemplate;
            identifier.target = Target;
            identifier.creator = Creator;

            return identifier;
        }

        public static DocumentUniqueIdentifier Create(DiscoDataContext Database, string DocumentTemplateId, string TargetId, string CreatorId, DateTime TimeStamp, int PageIndex)
        {
            var deploymentChecksum = Database.DiscoConfiguration.DeploymentChecksum;

            return new DocumentUniqueIdentifier(Database, CurrentVersion, deploymentChecksum, DocumentTemplateId, TargetId, CreatorId, TimeStamp, PageIndex, null);
        }

        public static DocumentUniqueIdentifier Create(DiscoDataContext Database, string DocumentTemplateId, AttachmentTypes attachmentType, string TargetId, string CreatorId, DateTime TimeStamp, int PageIndex)
        {
            var deploymentChecksum = Database.DiscoConfiguration.DeploymentChecksum;

            return new DocumentUniqueIdentifier(Database, CurrentVersion, deploymentChecksum, DocumentTemplateId, TargetId, CreatorId, TimeStamp, PageIndex, attachmentType);
        }

        public static DocumentUniqueIdentifier Parse(DiscoDataContext Database, byte[] UniqueIdentifier)
        {
            DocumentUniqueIdentifier identifier;
            if (TryParse(Database, UniqueIdentifier, out identifier))
            {
                return identifier;
            }
            else
            {
                throw new FormatException("Invalid Document Unique Identifier");
            }
        }

        public static DocumentUniqueIdentifier Parse(DiscoDataContext Database, string UniqueIdentifier)
        {
            DocumentUniqueIdentifier identifier;
            if (TryParse(Database, UniqueIdentifier, out identifier))
            {
                return identifier;
            }
            else
            {
                throw new FormatException("Invalid Document Unique Identifier");
            }
        }

        public static bool TryParse(DiscoDataContext Database, string UniqueIdentifier, out DocumentUniqueIdentifier Identifier)
        {
            if (IsDocumentUniqueIdentifier(UniqueIdentifier))
            {
                var components = UniqueIdentifier.Split('|');

                // Version 0, Version 1 Handling
                if ((components.Length == 7 || components.Length == 6) &&
                    (components[1].Equals("1", StringComparison.Ordinal) || components[1].Equals("AT", StringComparison.OrdinalIgnoreCase)))
                {
                    var documentTemplateId = components[2];
                    var targetId = components[3];
                    var creatorId = components[4];
                    var timeStamp = DateTime.Parse(components[5]);
                    var page = 0;
                    if (components.Length == 7)
                    {
                        page = int.Parse(components[6]);
                    }

                    Identifier = new DocumentUniqueIdentifier(Database, 1, -1, documentTemplateId, targetId, creatorId, timeStamp, page, null);
                    return true;
                }
            }

            Identifier = null;
            return false;
        }

        public static bool TryParse(DiscoDataContext Database, byte[] Data, out DocumentUniqueIdentifier Identifier)
        {
            if (IsDocumentUniqueIdentifier(Data))
            {
                // first 4 bit indicate version
                var version = Data[1] >> 4;

                // Version 2
                if (version == 2)
                {
                    // Byte | Meaning
                    // 0    | magic number = 0xC4
                    // 1    | bits 0-3 = version;
                    //      |      4   = flag: has document template id
                    //      |      5-6 = (01 = device attachment, 10 = job attachment, 11 = user attachment)
                    //      |      7   = not used
                    // 2-3  | deployment checksum (int16)
                    // 4-7  | timestamp (uint32 unix epoch)
                    // 8-9  | page index (uint16)
                    // 10   | creator id encoded
                    // ?    | target id encoded
                    // ?    | document template id encoded (optional based on flag)

                    // read flags
                    var flags = Data[1] & 0x0F;

                    // read deployment checksum
                    short deploymentChecksum = (short)((Data[2] << 8) | Data[3]);

                    // read timestamp
                    var timeStampEpoch = ((long)Data[4] << 24) |
                        ((long)Data[5] << 16) |
                        ((long)Data[6] << 8) |
                        Data[7];

                    // write page index
                    var pageIndex = (Data[8] << 8) | Data[9];

                    var position = 10;

                    // write creator id
                    var creatorId = DocumentUniqueIdentifierExtensions.BinaryDecode(Data, position, out position);

                    // write target id
                    var targetId = DocumentUniqueIdentifierExtensions.BinaryDecode(Data, position, out position);

                    // write document template id
                    string documentTemplateId = null;

                    // Has document template id flag
                    if ((flags & 0x8) == 0x8)
                    {
                        documentTemplateId = DocumentUniqueIdentifierExtensions.BinaryDecode(Data, position, out position);
                    }

                    AttachmentTypes? attachmentType = null;
                    switch (flags & 0x6)
                    {
                        case 0x2:
                            attachmentType = AttachmentTypes.Device;
                            break;
                        case 0x4:
                            attachmentType = AttachmentTypes.Job;
                            break;
                        case 0x6:
                            attachmentType = AttachmentTypes.User;
                            break;
                    }

                    var timeStamp = DateTime.FromFileTimeUtc(116444736000000000L).AddTicks(TimeSpan.TicksPerSecond * timeStampEpoch).ToLocalTime();

                    Identifier = new DocumentUniqueIdentifier(Database, version, deploymentChecksum, documentTemplateId, targetId, creatorId, timeStamp, pageIndex, attachmentType);
                    return true;
                }
            }

            Identifier = null;
            return false;
        }

        public static bool IsDocumentUniqueIdentifier(string Identifier)
        {
            return Identifier != null && Identifier.StartsWith("Disco|", StringComparison.OrdinalIgnoreCase);
        }

        public static bool IsDocumentUniqueIdentifier(byte[] Identifier)
        {
            // Identifier[0] = 0xC4; Magic number to identify Disco ICT QR Codes
            return Identifier != null && Identifier.Length > 2 && Identifier[0] == MagicNumber;
        }
    }
}
