using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Linq;

namespace Fody.DIY
{
    public class Obfuscate
    {
        public static void ObfuscateNames(AssemblyDefinition assemblyDefinition)
        {
            char newModuleName = ' ';
            foreach (var moduleDefinition in assemblyDefinition.Modules)
            {
                moduleDefinition.Name = newModuleName.ToString();
                char newTypeName = '\0';
                foreach (var typeDefinition in moduleDefinition.Types)
                {
                    typeDefinition.Name = newTypeName.ToString();
                    char newMethodName = '\0';
                    foreach (var methodDefinition in typeDefinition.Methods)
                    {
                        if (methodDefinition.IsConstructor)
                            continue;
                        methodDefinition.Name = newMethodName.ToString();
                        newMethodName++;
                    }
                    char newFieldName = '\0';
                    foreach (var fieldDefinition in typeDefinition.Fields)
                    {
                        fieldDefinition.Name = newFieldName.ToString();
                        newFieldName++;
                    }
                    newTypeName++;
                }
                newModuleName++;
            }
        }

        public static void ObfuscateString(AssemblyDefinition assemblyDefinition)
        {
            var random = new Random((int)DateTime.Now.Ticks);
            var typeNames = assemblyDefinition.MainModule.Types;
            foreach (var typeName in typeNames)
            {
                var methodNames = typeName.Methods;
                var method = CreateDecryptMethod(typeName, typeName.Module);
                foreach (var methodName in methodNames)
                {
                    for (int i = 0; i < methodName.Body.Instructions.Count; i++)
                    {
                        var instruction = methodName.Body.Instructions[i];
                        if (instruction.Operand != null &&
                            instruction.OpCode.OperandType == OperandType.InlineString)
                        {
                            var worker = methodName.Body.GetILProcessor();
                            int rand = random.Next(255);
                            instruction.Operand =
                                instruction.Operand.ToString().Select(c => ((char)(c + rand)).ToString()).Aggregate(
                                    (c1, c2) => c1 + c2);
                            worker.InsertAfter(instruction, worker.Create(OpCodes.Ldc_I4, rand));
                            var inst = worker.Create(OpCodes.Call, method.Resolve());
                            worker.InsertAfter(instruction.Next, inst);
                        }
                    }
                }

            }
        }

        private static MethodDefinition CreateDecryptMethod(TypeDefinition typeName, ModuleDefinition module)
        {
            var methodDef = new MethodDefinition("zzz", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, module.Import(typeof(string)));
            typeName.Methods.Add(methodDef);
            methodDef.Parameters.Add(new ParameterDefinition("data", ParameterAttributes.None,
                                                             module.Import(typeof(string))));
            methodDef.Parameters.Add(new ParameterDefinition("ile", ParameterAttributes.None, module.Import(typeof(int))));

            var worker = methodDef.Body.GetILProcessor();

            //create temporary vars
            worker.Body.InitLocals = true;
            var resultVar = new VariableDefinition("result", module.Import(typeof(string)));
            worker.Body.Variables.Add(resultVar);

            var charVar = new VariableDefinition("znak", module.Import(typeof(char)));
            worker.Body.Variables.Add(charVar);

            var tempString = new VariableDefinition(module.Import(typeof(string)));
            worker.Body.Variables.Add(tempString);
            var tempString2 = new VariableDefinition(module.Import(typeof(string)));
            worker.Body.Variables.Add(tempString2);

            var tempCount = new VariableDefinition(module.Import(typeof(int)));
            worker.Body.Variables.Add(tempCount);

            var tempBool = new VariableDefinition(module.Import(typeof(bool)));
            worker.Body.Variables.Add(tempBool);

            var stringEmptyDef = module.Import(typeof(string)).Resolve().Fields.Where(f => f.Name == "Empty").First();
            var stringEmpty = module.Import(stringEmptyDef);

            // string result = string.Empty;
            worker.Append(worker.Create(OpCodes.Ldsfld, stringEmpty));
            worker.Append(worker.Create(OpCodes.Stloc_0));

            //loop
            // foreach(char znak in data)
            //{
            worker.Append(worker.Create(OpCodes.Ldarg_0));
            worker.Append(worker.Create(OpCodes.Stloc_3));
            worker.Append(worker.Create(OpCodes.Ldc_I4_0));

            var beforeJump = worker.Create(OpCodes.Stloc_S, tempCount);
            worker.Append(beforeJump);
            //jmp

            //var znak = data.Chars
            var loopStart = worker.Create(OpCodes.Ldloc_3);
            worker.Append(loopStart);
            worker.Append(worker.Create(OpCodes.Ldloc_S, tempCount));
            var getCharsDef =
                module.Import(typeof(string)).Resolve().Properties.Where(m => m.Name == "Chars").First().GetMethod;
            var getChars = module.Import(getCharsDef);
            worker.Append(worker.Create(OpCodes.Callvirt, getChars));
            worker.Append(worker.Create(OpCodes.Stloc_1));

            worker.Append(worker.Create(OpCodes.Ldloc_0));
            worker.Append(worker.Create(OpCodes.Ldloc_1));

            worker.Append(worker.Create(OpCodes.Ldarg_1)); // ile
            worker.Append(worker.Create(OpCodes.Sub));
            worker.Append(worker.Create(OpCodes.Conv_U2));

            worker.Append(worker.Create(OpCodes.Box, module.Import(typeof(char))));

            //result += znak;
            var concatDef = module.Import(typeof(string)).Resolve().Methods.Where(m => m.Name == "Concat" && m.Parameters.Count == 2).First();
            var concat = module.Import(concatDef);
            worker.Append(worker.Create(OpCodes.Call, concat));
            worker.Append(worker.Create(OpCodes.Stloc_0));

            // i = i + 1;
            worker.Append(worker.Create(OpCodes.Ldloc_S, tempCount));
            worker.Append(worker.Create(OpCodes.Ldc_I4_1));
            worker.Append(worker.Create(OpCodes.Add));
            worker.Append(worker.Create(OpCodes.Stloc_S, tempCount));

            var checkLength = worker.Create(OpCodes.Ldloc_S, tempCount);
            worker.Append(checkLength);

            worker.Append(worker.Create(OpCodes.Ldloc_3));
            var getLengthDef =
                module.Import(typeof(string)).Resolve().Properties.Where(p => p.Name == "Length").First().GetMethod;

            var getLength = module.Import(getLengthDef);
            worker.Append(worker.Create(OpCodes.Callvirt, getLength));
            worker.Append(worker.Create(OpCodes.Clt));

            worker.Append(worker.Create(OpCodes.Stloc_S, tempBool));
            worker.Append(worker.Create(OpCodes.Ldloc_S, tempBool));
            worker.Append(worker.Create(OpCodes.Brtrue_S, loopStart));

            //
            worker.InsertAfter(beforeJump, worker.Create(OpCodes.Br_S, checkLength));

            worker.Append(worker.Create(OpCodes.Ldloc_0));
            worker.Append(worker.Create(OpCodes.Stloc_2));

            var code = worker.Create(OpCodes.Ldloc_2);
            worker.Append(code);

            worker.InsertBefore(code, worker.Create(OpCodes.Br_S, code));

            worker.Append(worker.Create(OpCodes.Ret));

            return methodDef;
        }
    }
}
