// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License

namespace Azure.Iot.Operations.SchemaGenerator
{
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using Azure.Iot.Operations.CodeGeneration;
    using Azure.Iot.Operations.TDParser;
    using Azure.Iot.Operations.TDParser.Model;

    public static class SchemaGenerator
    {
        public static Dictionary<SerializationFormat, List<GeneratedItem>> GenerateSchemas(IEnumerable<IResolvingThing> resolvingThings, string projectName, DirectoryInfo workingDir)
        {
            Dictionary<string, ISchemaTemplateTransform> transforms = new();

            foreach (IResolvingThing resolvingThing in resolvingThings.Where(r => r.ParsedThing.Thing.Type?.Value.Value != TDValues.TypeThingDescription))
            {
                ParsedThing parsedThing = resolvingThing.ParsedThing;

                Dictionary<string, List<SchemaSpec>> schemaSpecs = new();
                Dictionary<string, HashSet<SerializationFormat>> referencedSchemas = new();

                PropertySchemaGenerator.GeneratePropertySchemas(resolvingThing, projectName, schemaSpecs, referencedSchemas);
                ActionSchemaGenerator.GenerateActionSchemas(resolvingThing, projectName, schemaSpecs, referencedSchemas);
                EventSchemaGenerator.GenerateEventSchemas(resolvingThing, projectName, schemaSpecs, referencedSchemas);

                LocalSchemaResolver localSchemaResolver = new(parsedThing.ErrorReporter, parsedThing.Thing.SchemaDefinitions?.Entries);
                Dictionary<string, SchemaSpec> closedSchemaSpecs = ComputeClosedSchemaSpecs(parsedThing.ErrorReporter, parsedThing.Thing, parsedThing.SchemaNamer, localSchemaResolver, schemaSpecs, referencedSchemas);

                SchemaTransformFactory transformFactory = new(parsedThing.SchemaNamer, workingDir, localSchemaResolver);

                foreach (KeyValuePair<string, SchemaSpec> schemaSpec in closedSchemaSpecs)
                {
                    if (transformFactory.TryGetSchemaTransform(schemaSpec.Key, schemaSpec.Value, out ISchemaTemplateTransform? transform))
                    {
                        transforms[transform.FileName] = transform;
                    }
                }
            }

            Dictionary<SerializationFormat, List<GeneratedItem>> generatedSchemas = new();

            foreach (KeyValuePair<string, ISchemaTemplateTransform> transform in transforms)
            {
                if (!generatedSchemas.TryGetValue(transform.Value.Format, out List<GeneratedItem>? schemas))
                {
                    schemas = new();
                    generatedSchemas[transform.Value.Format] = schemas;
                }

                schemas.Add(new GeneratedItem(transform.Value.TransformText(), transform.Key));
            }

            return generatedSchemas;
        }

        private static Dictionary<string, SchemaSpec> ComputeClosedSchemaSpecs(ErrorReporter errorReporter, TDThing thing, SchemaNamer schemaNamer, LocalSchemaResolver localSchemaResolver, Dictionary<string, List<SchemaSpec>> schemaSpecs, Dictionary<string, HashSet<SerializationFormat>> referencedSchemas)
        {
            Dictionary<string, SchemaSpec> closedSchemaSpecs = new();

            foreach (KeyValuePair<string, HashSet<SerializationFormat>> referencedSchema in referencedSchemas)
            {
                foreach (SerializationFormat format in referencedSchema.Value)
                {
                    if (thing.SchemaDefinitions?.Entries?.TryGetValue(referencedSchema.Key, out ValueTracker<TDDataSchema>? dataSchema) ?? false)
                    {
                        ComputeClosureOfDataSchema(errorReporter, schemaNamer, localSchemaResolver, thing.Title!.Value.Value, referencedSchema.Key, dataSchema, format, closedSchemaSpecs, preserveBackupNameForLocalRef: true, referenceChain: [referencedSchema.Key]);
                    }
                }
            }

            foreach (KeyValuePair<string, List<SchemaSpec>> schemaSpec in schemaSpecs)
            {
                foreach (SchemaSpec spec in schemaSpec.Value)
                {
                    ComputeClosureOfSchemaSpec(errorReporter, schemaNamer, localSchemaResolver, thing.Title!.Value.Value, schemaSpec.Key, spec, closedSchemaSpecs);
                }
            }

            return closedSchemaSpecs;
        }

        private static void ComputeClosureOfSchemaSpec(ErrorReporter errorReporter, SchemaNamer schemaNamer, LocalSchemaResolver localSchemaResolver, string thingName, string schemaName, SchemaSpec schemaSpec, Dictionary<string, SchemaSpec> closedSchemaSpecs)
        {
            if (IsLocalDuplicate(errorReporter, schemaName, schemaSpec, closedSchemaSpecs))
            {
                return;
            }

            closedSchemaSpecs[schemaName] = schemaSpec;
            errorReporter.RegisterNameInThing(schemaName, thingName, schemaSpec.TokenIndex);

            if (schemaSpec is ObjectSpec objectSpec)
            {
                foreach (KeyValuePair<string, FieldSpec> field in objectSpec.Fields.Where(f => !f.Value.Inherited))
                {
                    ComputeClosureOfDataSchema(errorReporter, schemaNamer, localSchemaResolver, thingName, field.Value.BackupSchemaName, field.Value.Schema, schemaSpec.Format, closedSchemaSpecs);
                }
            }
        }

        private static void ComputeClosureOfDataSchema(ErrorReporter errorReporter, SchemaNamer schemaNamer, LocalSchemaResolver localSchemaResolver, string thingName, string backupName, ValueTracker<TDDataSchema> dataSchema, SerializationFormat format, Dictionary<string, SchemaSpec> closedSchemaSpecs, bool preserveBackupNameForLocalRef = false, IReadOnlyCollection<string>? referenceChain = null)
        {
            if (!localSchemaResolver.TryResolve(dataSchema, referenceChain, out ValueTracker<TDDataSchema>? resolvedSchema, out string? directSchemaKey, out List<string> resolvedReferenceChain))
            {
                return;
            }

            ValueTracker<TDDataSchema> activeSchema = resolvedSchema!;
            string effectiveBackupName = directSchemaKey != null && !preserveBackupNameForLocalRef ? directSchemaKey : backupName;

            if (IsProxy(activeSchema.Value))
            {
                return;
            }

            string schemaName = schemaNamer.ApplyBackupSchemaName(activeSchema.Value.Title?.Value.Value, effectiveBackupName);

            if (SchemaSpec.TryCreateFromDataSchema(errorReporter, schemaNamer, activeSchema, format, effectiveBackupName, out SchemaSpec? schemaSpec))
            {
                if (IsLocalDuplicate(errorReporter, schemaName, schemaSpec, closedSchemaSpecs))
                {
                    return;
                }

                closedSchemaSpecs[schemaName] = schemaSpec;
                errorReporter.RegisterNameInThing(schemaName, thingName, schemaSpec.TokenIndex);
            }

            if (activeSchema.Value.Properties?.Entries != null)
            {
                foreach (KeyValuePair<string, ValueTracker<TDDataSchema>> property in activeSchema.Value.Properties.Entries)
                {
                    ComputeClosureOfDataSchema(errorReporter, schemaNamer, localSchemaResolver, thingName, schemaNamer.GetBackupSchemaName(schemaName, property.Key), property.Value, format, closedSchemaSpecs, referenceChain: resolvedReferenceChain);
                }
            }
            else if (activeSchema.Value.Items?.Value != null)
            {
                ComputeClosureOfDataSchema(errorReporter, schemaNamer, localSchemaResolver, thingName, effectiveBackupName, activeSchema.Value.Items, format, closedSchemaSpecs, referenceChain: resolvedReferenceChain);
            }
            else if (activeSchema.Value.AdditionalProperties?.Value != null)
            {
                ComputeClosureOfDataSchema(errorReporter, schemaNamer, localSchemaResolver, thingName, effectiveBackupName, activeSchema.Value.AdditionalProperties, format, closedSchemaSpecs, referenceChain: resolvedReferenceChain);
            }
        }

        private static bool IsProxy(TDDataSchema dataSchema)
        {
            return dataSchema.Type?.Value.Value == TDValues.TypeObject && dataSchema.AdditionalProperties == null && dataSchema.Properties == null;
        }

        private static bool IsLocalDuplicate(ErrorReporter errorReporter, string schemaName, SchemaSpec schemaSpec, Dictionary<string, SchemaSpec> closedSchemaSpecs)
        {
            if (!closedSchemaSpecs.TryGetValue(schemaName, out SchemaSpec? existingSpec))
            {
                return false;
            }

            if (existingSpec.GetType() != schemaSpec.GetType())
            {
                errorReporter.ReportError(ErrorCondition.Duplication, $"Schema name {schemaName} is duplicated on schema with different type.", schemaSpec.TokenIndex, existingSpec.TokenIndex);
                return false;
            }
            else if (existingSpec is ObjectSpec existingObjectSpec && schemaSpec is ObjectSpec newObjectSpec)
            {
                foreach (KeyValuePair<string, FieldSpec> field in existingObjectSpec.Fields)
                {
                    if (!newObjectSpec.Fields.TryGetValue(field.Key, out FieldSpec? newField))
                    {
                        errorReporter.ReportError(ErrorCondition.Duplication, $"Schema name {schemaName} is duplicated but schema has field '{field.Key}' not present in other schema.", field.Value.Schema.TokenIndex, newObjectSpec.TokenIndex);
                        return false;
                    }
                }

                foreach (KeyValuePair<string, FieldSpec> field in newObjectSpec.Fields)
                {
                    if (!existingObjectSpec.Fields.TryGetValue(field.Key, out FieldSpec? extantField))
                    {
                        errorReporter.ReportError(ErrorCondition.Duplication, $"Schema name {schemaName} is duplicated but schema has field '{field.Key}' not present in other schema.", field.Value.Schema.TokenIndex, existingObjectSpec.TokenIndex);
                        return false;
                    }
                }

                foreach (KeyValuePair<string, FieldSpec> field in newObjectSpec.Fields)
                {
                    FieldSpec existingFieldValue = existingObjectSpec.Fields[field.Key];
                    if (!field.Value.Equals(existingFieldValue))
                    {
                        errorReporter.ReportError(ErrorCondition.Duplication, $"Schema name {schemaName} is duplicated but field '{field.Key}' has different value.", field.Value.Schema.TokenIndex, existingFieldValue.Schema.TokenIndex);
                        return false;
                    }
                }

                return true;
            }
            else if (existingSpec is EnumSpec existingEnumSpec && schemaSpec is EnumSpec newEnumSpec)
            {
                foreach (string value in existingEnumSpec.Values)
                {
                    if (!newEnumSpec.Values.Contains(value))
                    {
                        errorReporter.ReportError(ErrorCondition.Duplication, $"Schema name {schemaName} is duplicated but schema has enum value '{value}' not present in other schema.", existingEnumSpec.TokenIndex, newEnumSpec.TokenIndex);
                        return false;
                    }
                }

                foreach (string value in newEnumSpec.Values)
                {
                    if (!existingEnumSpec.Values.Contains(value))
                    {
                        errorReporter.ReportError(ErrorCondition.Duplication, $"Schema name {schemaName} is duplicated but schema has enum value '{value}' not present in other schema.", newEnumSpec.TokenIndex, existingEnumSpec.TokenIndex);
                        return false;
                    }
                }

                return true;
            }

            return false;
        }
    }
}
