﻿using System;
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;

class OnExceptionProcessor
{
    public MethodDefinition Method;
    public Action FoundUsageInType;
    public FieldReference LoggerField;
    public ModuleWeaver ModuleWeaver;
    MethodBody body;

    VariableDefinition paramsArrayVariable;
    VariableDefinition messageVariable;
    VariableDefinition exceptionVariable;
    AttributeFinder attributeFinder;

    public void Process()
    {
        attributeFinder = new AttributeFinder(Method);
        if (!attributeFinder.Found)
        {
            return;
        }
        Method.ThrowIfIsAsync();

        FoundUsageInType();
        ContinueProcessing();
    }

    void ContinueProcessing()
    {
        body = Method.Body;

        body.SimplifyMacros();

        var ilProcessor = body.GetILProcessor();

        var returnFixer = new ReturnFixer
        {
            Method = Method
        };
        returnFixer.MakeLastStatementReturn();

        exceptionVariable = new VariableDefinition(ModuleWeaver.ExceptionType);
        body.Variables.Add(exceptionVariable);
        messageVariable = new VariableDefinition(ModuleWeaver.TypeSystem.StringReference);
        body.Variables.Add(messageVariable);
        paramsArrayVariable = new VariableDefinition(ModuleWeaver.ObjectArray);
        body.Variables.Add(paramsArrayVariable);


        var tryCatchLeaveInstructions = Instruction.Create(OpCodes.Leave, returnFixer.NopBeforeReturn);

        var methodBodyFirstInstruction = GetMethodBodyFirstInstruction();

        var catchInstructions = GetCatchInstructions().ToList();

        ilProcessor.InsertBefore(returnFixer.NopBeforeReturn, tryCatchLeaveInstructions);

        ilProcessor.InsertBefore(returnFixer.NopBeforeReturn, catchInstructions);

        var handler = new ExceptionHandler(ExceptionHandlerType.Catch)
        {
            CatchType = ModuleWeaver.ExceptionType,
            TryStart = methodBodyFirstInstruction,
            TryEnd = tryCatchLeaveInstructions.Next,
            HandlerStart = catchInstructions.First(),
            HandlerEnd = catchInstructions.Last().Next
        };

        body.ExceptionHandlers.Add(handler);

        body.InitLocals = true;
        body.OptimizeMacros();
    }

    Instruction GetMethodBodyFirstInstruction()
    {
        if (Method.IsConstructor)
        {
            return body.Instructions.First(i => i.OpCode == OpCodes.Call).Next;
        }
        return body.Instructions.First();
    }

    IEnumerable<Instruction> GetCatchInstructions()
    {
        yield return Instruction.Create(OpCodes.Stloc, exceptionVariable);
        var messageLdstr = Instruction.Create(OpCodes.Ldstr, "");
        yield return messageLdstr;
        yield return Instruction.Create(OpCodes.Ldc_I4, Method.Parameters.Count);
        yield return Instruction.Create(OpCodes.Newarr, ModuleWeaver.TypeSystem.ObjectReference);
        yield return Instruction.Create(OpCodes.Stloc, paramsArrayVariable);

        var paramsFormatBuilder = new ParamsFormatBuilder(Method, paramsArrayVariable);

        foreach (var instruction in paramsFormatBuilder.Instructions)
        {
            yield return instruction;
        }
        messageLdstr.Operand = paramsFormatBuilder.MessageBuilder.ToString();

        yield return Instruction.Create(OpCodes.Ldloc, paramsArrayVariable);
        yield return Instruction.Create(OpCodes.Call, ModuleWeaver.FormatMethod);
        yield return Instruction.Create(OpCodes.Stloc, messageVariable);

        if (attributeFinder.FoundTrace)
        {
            foreach (var instruction in AddWrite(ModuleWeaver.TraceExceptionMethod, ModuleWeaver.IsTraceEnabledMethod))
            {
                yield return instruction;
            }
        }
        if (attributeFinder.FoundDebug)
        {
            foreach (var instruction in AddWrite(ModuleWeaver.DebugExceptionMethod, ModuleWeaver.IsDebugEnabledMethod))
            {
                yield return instruction;
            }
        }
        if (attributeFinder.FoundInfo)
        {
            foreach (var instruction in AddWrite(ModuleWeaver.InfoExceptionMethod, ModuleWeaver.IsInfoEnabledMethod))
            {
                yield return instruction;
            }
        }
        if (attributeFinder.FoundWarn)
        {
            foreach (var instruction in AddWrite(ModuleWeaver.WarnExceptionMethod, ModuleWeaver.IsWarnEnabledMethod))
            {
                yield return instruction;
            }
        }
        if (attributeFinder.FoundError)
        {
            foreach (var instruction in AddWrite(ModuleWeaver.ErrorExceptionMethod, ModuleWeaver.IsErrorEnabledMethod))
            {
                yield return instruction;
            }
        }
        if (attributeFinder.FoundFatal)
        {
            foreach (var instruction in AddWrite(ModuleWeaver.FatalExceptionMethod, ModuleWeaver.IsFatalEnabledMethod))
            {
                yield return instruction;
            }
        }

        yield return Instruction.Create(OpCodes.Rethrow);
    }

    IEnumerable<Instruction> AddWrite(MethodReference writeMethod, MethodReference isEnabledMethod)
    {
        var sectionNop = Instruction.Create(OpCodes.Nop);
        yield return Instruction.Create(OpCodes.Ldsfld, LoggerField);
        yield return Instruction.Create(OpCodes.Callvirt, isEnabledMethod);
        yield return Instruction.Create(OpCodes.Brfalse_S, sectionNop);
        yield return Instruction.Create(OpCodes.Ldsfld, LoggerField);
        yield return Instruction.Create(OpCodes.Ldloc, messageVariable);
        yield return Instruction.Create(OpCodes.Ldloc, exceptionVariable);
        yield return Instruction.Create(OpCodes.Callvirt, writeMethod);
        yield return sectionNop;
    }
}