// ==================== PROJECT STRUCTURE ====================
/*
Solution: ReflectionAssignment
├── MathsLib (Class Library)
├── AttributesLib (Class Library)  
├── EntityLib (Class Library)
├── MathsConsole (Console Application)
└── ORMConsole (Console Application)
*/

// ==================== 1. MathsLib Class Library ====================
// File: MathsLib/Maths.cs
using System;

namespace MathsLib
{
    public class Maths
    {
        public double Sum(double a, double b)
        {
            return a + b;
        }

        public double Subtract(double a, double b)
        {
            return a - b;
        }

        public double Multiply(double a, double b)
        {
            return a * b;
        }

        public double Divide(double a, double b)
        {
            if (b == 0)
                throw new DivideByZeroException("Cannot divide by zero");
            return a / b;
        }

        public void DisplayOperations()
        {
            Console.WriteLine("Available Operations:");
            Console.WriteLine("1. Sum");
            Console.WriteLine("2. Subtract");
            Console.WriteLine("3. Multiply");
            Console.WriteLine("4. Divide");
        }
    }
}

// ==================== 2. AttributesLib Class Library ====================
// File: AttributesLib/CustomAttributes.cs
using System;

namespace AttributesLib
{
    // Custom attribute for marking classes as data tables
    [AttributeUsage(AttributeTargets.Class)]
    public class DataTableAttribute : Attribute
    {
        public string TableName { get; set; }

        public DataTableAttribute(string tableName)
        {
            TableName = tableName;
        }
    }

    // Custom attribute for marking properties as data columns
    [AttributeUsage(AttributeTargets.Property)]
    public class DataColumnAttribute : Attribute
    {
        public string ColumnName { get; set; }
        public string DataType { get; set; }

        public DataColumnAttribute(string columnName, string dataType = "")
        {
            ColumnName = columnName;
            DataType = dataType;
        }
    }

    // Custom attribute for marking properties as key columns (Primary Key)
    [AttributeUsage(AttributeTargets.Property)]
    public class KeyColumnAttribute : Attribute
    {
        public bool IsPrimaryKey { get; set; }

        public KeyColumnAttribute(bool isPrimaryKey = true)
        {
            IsPrimaryKey = isPrimaryKey;
        }
    }

    // Custom attribute for marking properties to be excluded from mapping
    [AttributeUsage(AttributeTargets.Property)]
    public class UnMappedAttribute : Attribute
    {
    }
}

// ==================== 3. EntityLib Class Library ====================
// File: EntityLib/Employee.cs
using System;
using AttributesLib;

namespace EntityLib
{
    [Serializable]
    [DataTable("Employees")]
    public class Employee
    {
        [KeyColumn(true)]
        [DataColumn("EmpNo", "INT")]
        public int EmpNo { get; set; }

        [DataColumn("Name", "VARCHAR(100)")]
        public string Name { get; set; }

        [DataColumn("Address", "VARCHAR(200)")]
        public string Address { get; set; }

        [DataColumn("Salary", "DECIMAL(10,2)")]
        public decimal Salary { get; set; }

        [UnMapped]
        public decimal AnnualSalary 
        { 
            get { return Salary * 12; }
        }

        [DataColumn("Designation", "VARCHAR(50)")]
        public string Designation { get; set; }

        public Employee()
        {
        }

        public Employee(int empNo, string name, string address, decimal salary, string designation)
        {
            EmpNo = empNo;
            Name = name;
            Address = address;
            Salary = salary;
            Designation = designation;
        }
    }
}

// File: EntityLib/Student.cs
using System;
using AttributesLib;

namespace EntityLib
{
    [Serializable]
    [DataTable("Students")]
    public class Student
    {
        [KeyColumn(true)]
        [DataColumn("RollNo", "INT")]
        public int RollNo { get; set; }

        [DataColumn("Name", "VARCHAR(100)")]
        public string Name { get; set; }

        [DataColumn("Address", "VARCHAR(200)")]
        public string Address { get; set; }

        [DataColumn("Course", "VARCHAR(50)")]
        public string Course { get; set; }

        public Student()
        {
        }

        public Student(int rollNo, string name, string address, string course)
        {
            RollNo = rollNo;
            Name = name;
            Address = address;
            Course = course;
        }
    }
}

// ==================== 4. MathsConsole Application ====================
// File: MathsConsole/Program.cs
using System;
using System.Reflection;
using System.IO;

namespace MathsConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("=== Maths Library Reflection Demo ===\\n");

            try
            {
                // Load the MathsLib assembly using reflection
                string mathsLibPath = "MathsLib.dll";
                Assembly mathsAssembly = Assembly.LoadFrom(mathsLibPath);
                
                // Get the Maths class type
                Type mathsType = mathsAssembly.GetType("MathsLib.Maths");
                
                // Create an instance of Maths class
                object mathsInstance = Activator.CreateInstance(mathsType);

                char choice;
                do
                {
                    Console.WriteLine("\\n=== Math Operations Menu ===");
                    Console.WriteLine("1. Sum");
                    Console.WriteLine("2. Subtract");
                    Console.WriteLine("3. Multiply");
                    Console.WriteLine("4. Divide");
                    Console.WriteLine("5. Show Available Methods");
                    Console.WriteLine("6. Exit");
                    Console.Write("Enter your choice (1-6): ");

                    choice = Console.ReadKey().KeyChar;
                    Console.WriteLine();

                    if (choice >= '1' && choice <= '4')
                    {
                        Console.Write("Enter first number: ");
                        double num1 = Convert.ToDouble(Console.ReadLine());

                        Console.Write("Enter second number: ");
                        double num2 = Convert.ToDouble(Console.ReadLine());

                        string methodName = "";
                        switch (choice)
                        {
                            case '1': methodName = "Sum"; break;
                            case '2': methodName = "Subtract"; break;
                            case '3': methodName = "Multiply"; break;
                            case '4': methodName = "Divide"; break;
                        }

                        // Get the method using reflection
                        MethodInfo method = mathsType.GetMethod(methodName);
                        
                        if (method != null)
                        {
                            try
                            {
                                // Invoke the method using reflection
                                object result = method.Invoke(mathsInstance, new object[] { num1, num2 });
                                Console.WriteLine($"Result: {num1} {GetOperatorSymbol(choice)} {num2} = {result}");
                            }
                            catch (TargetInvocationException ex)
                            {
                                Console.WriteLine($"Error: {ex.InnerException.Message}");
                            }
                        }
                        else
                        {
                            Console.WriteLine("Method not found!");
                        }
                    }
                    else if (choice == '5')
                    {
                        ShowAvailableMethods(mathsType);
                    }
                    else if (choice != '6')
                    {
                        Console.WriteLine("Invalid choice! Please try again.");
                    }

                } while (choice != '6');

                Console.WriteLine("Thank you for using Math Calculator!");
            }
            catch (FileNotFoundException)
            {
                Console.WriteLine("Error: MathsLib.dll not found. Please ensure the library is in the same directory.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }

        static string GetOperatorSymbol(char choice)
        {
            return choice switch
            {
                '1' => "+",
                '2' => "-",
                '3' => "*",
                '4' => "/",
                _ => "?"
            };
        }

        static void ShowAvailableMethods(Type mathsType)
        {
            Console.WriteLine("\\nAvailable Methods in Maths Class:");
            MethodInfo[] methods = mathsType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            
            foreach (MethodInfo method in methods)
            {
                ParameterInfo[] parameters = method.GetParameters();
                string paramStr = string.Join(", ", Array.ConvertAll(parameters, p => $"{p.ParameterType.Name} {p.Name}"));
                Console.WriteLine($"- {method.ReturnType.Name} {method.Name}({paramStr})");
            }
        }
    }
}

// ==================== 5. ORMConsole Application ====================
// File: ORMConsole/Program.cs
using System;
using System.Reflection;
using System.Linq;
using System.IO;
using System.Text;
using AttributesLib;
using EntityLib;

namespace ORMConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("=== Basic ORM Using Reflection ===\\n");

            try
            {
                // Test with Employee class
                Console.WriteLine("1. Processing Employee Class:");
                ProcessEntity(typeof(Employee));

                Console.WriteLine("\\n" + new string('=', 50) + "\\n");

                // Test with Student class
                Console.WriteLine("2. Processing Student Class:");
                ProcessEntity(typeof(Student));

                // Create sample objects and generate SQL
                Console.WriteLine("\\n" + new string('=', 50) + "\\n");
                Console.WriteLine("3. Generating SQL for Sample Data:");
                
                Employee emp = new Employee(101, "John Doe", "123 Main St", 50000, "Developer");
                GenerateInsertSQL(emp);

                Student student = new Student(1001, "Jane Smith", "456 Oak Ave", "Computer Science");
                GenerateInsertSQL(student);

                Console.WriteLine("\\n4. SQL Statements written to files:");
                Console.WriteLine("- Employee_SQL.txt");
                Console.WriteLine("- Student_SQL.txt");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }

        static void ProcessEntity(Type entityType)
        {
            Console.WriteLine($"Entity: {entityType.Name}");

            // Check if class is marked as Serializable
            bool isSerializable = entityType.IsSerializable;
            Console.WriteLine($"Is Serializable: {isSerializable}");

            if (!isSerializable)
            {
                Console.WriteLine("Class is not marked as Serializable. Skipping SQL generation.");
                return;
            }

            // Get DataTable attribute
            DataTableAttribute tableAttr = (DataTableAttribute)Attribute.GetCustomAttribute(entityType, typeof(DataTableAttribute));
            string tableName = tableAttr?.TableName ?? entityType.Name;
            Console.WriteLine($"Table Name: {tableName}");

            // Get all properties
            PropertyInfo[] properties = entityType.GetProperties();
            Console.WriteLine($"\\nAll Properties ({properties.Length}):");
            foreach (var prop in properties)
            {
                Console.WriteLine($"- {prop.PropertyType.Name} {prop.Name}");
            }

            // Remove unmapped properties
            var mappedProperties = RemoveUnmappedFields(properties);
            Console.WriteLine($"\\nMapped Properties ({mappedProperties.Length}):");
            foreach (var prop in mappedProperties)
            {
                DataColumnAttribute colAttr = (DataColumnAttribute)Attribute.GetCustomAttribute(prop, typeof(DataColumnAttribute));
                KeyColumnAttribute keyAttr = (KeyColumnAttribute)Attribute.GetCustomAttribute(prop, typeof(KeyColumnAttribute));
                
                string columnName = colAttr?.ColumnName ?? prop.Name;
                string dataType = colAttr?.DataType ?? "VARCHAR(50)";
                bool isPrimaryKey = keyAttr?.IsPrimaryKey ?? false;
                
                Console.WriteLine($"- {columnName} ({dataType}){(isPrimaryKey ? " [PRIMARY KEY]" : "")}");
            }

            // Generate CREATE TABLE SQL
            string createTableSQL = GenerateCreateTableSQL(entityType, tableName, mappedProperties);
            Console.WriteLine($"\\nGenerated CREATE TABLE SQL:");
            Console.WriteLine(createTableSQL);
        }

        static PropertyInfo[] RemoveUnmappedFields(PropertyInfo[] properties)
        {
            return properties.Where(prop => 
                Attribute.GetCustomAttribute(prop, typeof(UnMappedAttribute)) == null
            ).ToArray();
        }

        static string GenerateCreateTableSQL(Type entityType, string tableName, PropertyInfo[] properties)
        {
            StringBuilder sql = new StringBuilder();
            sql.AppendLine($"CREATE TABLE {tableName} (");

            string[] columnDefinitions = new string[properties.Length];
            string primaryKeyColumn = "";

            for (int i = 0; i < properties.Length; i++)
            {
                var prop = properties[i];
                DataColumnAttribute colAttr = (DataColumnAttribute)Attribute.GetCustomAttribute(prop, typeof(DataColumnAttribute));
                KeyColumnAttribute keyAttr = (KeyColumnAttribute)Attribute.GetCustomAttribute(prop, typeof(KeyColumnAttribute));

                string columnName = colAttr?.ColumnName ?? prop.Name;
                string dataType = colAttr?.DataType ?? GetDefaultDataType(prop.PropertyType);
                bool isPrimaryKey = keyAttr?.IsPrimaryKey ?? false;

                if (isPrimaryKey)
                {
                    primaryKeyColumn = columnName;
                    columnDefinitions[i] = $"    {columnName} {dataType} NOT NULL";
                }
                else
                {
                    columnDefinitions[i] = $"    {columnName} {dataType}";
                }
            }

            sql.AppendLine(string.Join(",\\n", columnDefinitions));

            if (!string.IsNullOrEmpty(primaryKeyColumn))
            {
                sql.AppendLine($",    PRIMARY KEY ({primaryKeyColumn})");
            }

            sql.AppendLine(");");

            return sql.ToString();
        }

        static string GetDefaultDataType(Type type)
        {
            return type.Name switch
            {
                "Int32" => "INT",
                "String" => "VARCHAR(100)",
                "Decimal" => "DECIMAL(10,2)",
                "DateTime" => "DATETIME",
                "Boolean" => "BIT",
                _ => "VARCHAR(50)"
            };
        }

        static void GenerateInsertSQL(object entity)
        {
            Type entityType = entity.GetType();
            
            // Check if serializable
            if (!entityType.IsSerializable)
            {
                Console.WriteLine($"Entity {entityType.Name} is not serializable. Skipping INSERT SQL generation.");
                return;
            }

            DataTableAttribute tableAttr = (DataTableAttribute)Attribute.GetCustomAttribute(entityType, typeof(DataTableAttribute));
            string tableName = tableAttr?.TableName ?? entityType.Name;

            PropertyInfo[] properties = entityType.GetProperties();
            PropertyInfo[] mappedProperties = RemoveUnmappedFields(properties);

            StringBuilder sql = new StringBuilder();
            StringBuilder columns = new StringBuilder();
            StringBuilder values = new StringBuilder();

            sql.AppendLine($"INSERT INTO {tableName} (");

            for (int i = 0; i < mappedProperties.Length; i++)
            {
                var prop = mappedProperties[i];
                DataColumnAttribute colAttr = (DataColumnAttribute)Attribute.GetCustomAttribute(prop, typeof(DataColumnAttribute));
                string columnName = colAttr?.ColumnName ?? prop.Name;

                if (i > 0)
                {
                    columns.Append(", ");
                    values.Append(", ");
                }

                columns.Append(columnName);
                
                object value = prop.GetValue(entity);
                if (value == null)
                {
                    values.Append("NULL");
                }
                else if (prop.PropertyType == typeof(string))
                {
                    values.Append($"'{value.ToString().Replace("'", "''")}'");
                }
                else
                {
                    values.Append(value.ToString());
                }
            }

            sql.AppendLine($"    {columns}");
            sql.AppendLine(") VALUES (");
            sql.AppendLine($"    {values}");
            sql.AppendLine(");");

            Console.WriteLine($"\\nINSERT SQL for {entityType.Name}:");
            Console.WriteLine(sql.ToString());

            // Write to file
            string fileName = $"{entityType.Name}_SQL.txt";
            File.WriteAllText(fileName, sql.ToString());
            Console.WriteLine($"SQL written to: {fileName}");
        }
    }
}

// ==================== Compilation Instructions ====================
/*
To compile and run this solution:

1. Create MathsLib.dll:
   csc /target:library /out:MathsLib.dll MathsLib/Maths.cs

2. Create AttributesLib.dll:
   csc /target:library /out:AttributesLib.dll AttributesLib/CustomAttributes.cs

3. Create EntityLib.dll:
   csc /target:library /reference:AttributesLib.dll /out:EntityLib.dll EntityLib/Employee.cs EntityLib/Student.cs

4. Compile MathsConsole:
   csc /reference:MathsLib.dll MathsConsole/Program.cs

5. Compile ORMConsole:
   csc /reference:AttributesLib.dll;EntityLib.dll ORMConsole/Program.cs

Run the applications:
- MathsConsole.exe (for Part A)
- ORMConsole.exe (for Part B)
*/