//
// License compiler for mono
//
// Authors:
//   Carlo Kok (ck@remobjects.com)
//
// (C) 2009 RemObjects Software
//

//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mono.Options;
using System.ComponentModel.Design;
using System.IO;
using System.Reflection;
using System.Globalization;
using System.ComponentModel;
using System.Runtime.Serialization.Formatters.Binary;

namespace LC
{
    public class LCLicenseContext : DesigntimeLicenseContext
    {
        public string OutputFilename { get; set; }
        public string LicxFilename { get; set; }
    }

    class Program
    {
        static List<String> privatePaths = new List<string>();

        static int Main(string[] args)
        {
            bool verbose = false;
            string target = null;
            string complist = null;
            string targetdir = ".";
            List<string> references = new List<string>();

            bool nologo = false;
            bool help = false;
            OptionSet p = new OptionSet() {
                {"v|verbose", "Verbose output", v => verbose = v!= null },
                {"t|target=", "Target assembly name", v => target = v },
                {"c|complist=","licx file to compile", v => complist = v },
                {"i|load=", "Reference to load", v=> {if (v != null) references.Add(v);}},
                {"o|outdir=", "Output directory for the .licenses file", v=> targetdir = v },
                {"nologo", "Do not display logo", v=> nologo = null != v },
                {"h|?|help", "Show help", v=>help = v != null }
            };
            List<string> extra;
            try
            {
                extra = p.Parse(args);
            }
            catch(OptionException e) 
            {
                Console.WriteLine("lc: " + e.Message);
                Console.WriteLine("try lc --help for more information");
                return 1;
            }
            if (!nologo) {
                Console.WriteLine("Mono License Compiler");
                Console.WriteLine("Copyright (c) 2009 by RemObjects Software");
            }
            if (help) {
                Console.WriteLine();
                Console.WriteLine("lc -c filename -t targetassembly [-i references] [-v] [-o] [-nologo]");
                Console.WriteLine();
                Console.WriteLine("Options:");
                p.WriteOptionDescriptions(Console.Out);
                return 1;
            }
            if (extra.Count > 0) {
                Console.WriteLine("Unexpected arguments passed on cmd line");
                return 1;
            }
            if (target == null || complist == null){
                Console.WriteLine("No target/complist passed");
                return 1;
            }
            try {
                if (!File.Exists(complist)) {
                    Console.WriteLine("Could not find file: "+complist);
                    return 1;
                }

                LCLicenseContext ctx = new LCLicenseContext();
                ctx.LicxFilename = complist;
                if (verbose) Console.WriteLine("Input file: "+complist);
                ctx.OutputFilename = Path.Combine(targetdir ??".", target)+".licenses";
                if (verbose) Console.WriteLine("Output filename: "+ctx.OutputFilename);
                AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
                privatePaths.Add(".");
                Dictionary<string, Assembly> loaded = new Dictionary<string, Assembly>();
                foreach (string reference in references) {
                    string path = Path.GetDirectoryName(reference);
                    if (!privatePaths.Contains(path))
                    {
                        if (verbose) Console.WriteLine("Adding " + Path.GetDirectoryName(reference) + " to private paths");
                        privatePaths.Add(path);
                       }
                    Assembly asm = Assembly.LoadFrom(reference);
                    loaded.Add(asm.GetName().Name, asm);
                    if (verbose) Console.WriteLine("Loaded assembly: "+asm.GetName().ToString());

                }

                using (StreamReader sr = new StreamReader(complist))
                {
                    int lineno = 0;
                    string line = "";
                    while (sr.Peek() != -1)
                    {
                        try
                        {
                            line = sr.ReadLine();
                            if (line == null || line == "" || line[0] == '#' ) continue;
                            if (verbose) Console.WriteLine("Generating license for: "+line);

                            string[] sLine = line.Split(new char[] { ',' }, 2);
                            Type stype = null;
                            if (sLine.Length == 1)
                            {
                                stype = Type.GetType(line, false, true);
                                if (stype == null)
                                {
                                    foreach (KeyValuePair<string, Assembly> et in loaded)
                                    {
                                        stype = et.Value.GetType(sLine[0], false, true);
                                        if (stype != null) {
                                            if (verbose) Console.WriteLine("Found type in "+et.Key);
                                            break;
                                        }
                                    }
                                }
                            }
                            else
                            {
                                if (sLine[1].IndexOf(',') >= 0)
                                {
                                    stype = Type.GetType(line, false, true);
                                }
                                else
                                {
                                    string s = sLine[1].Trim();
                                    foreach (KeyValuePair<string, Assembly> et in loaded)
                                    {
                                        if (String.Compare(et.Key, s, true, CultureInfo.InvariantCulture) == 0)
                                        {
                                            stype = et.Value.GetType(sLine[0], false, true);
                                            if (stype != null) {
                                                if (verbose) Console.WriteLine("Found type in "+et.Key);
                                                break;
                                            }
                                        }
                                    }
                                    if (stype == null)
                                    {
                                        foreach (KeyValuePair<string, Assembly> et in loaded)
                                        {
                                            stype = et.Value.GetType(sLine[0], false, true);
                                            if (stype != null) {
                                                if (verbose) Console.WriteLine("Found type in "+et.Key);
                                                break;
                                            }
                                        }
                                    }
                                }
                            }
                            if (stype == null)
                                throw new Exception("Unable to find type: " + line);
                            LicenseManager.CreateWithContext(stype, ctx);
                        }
                        catch(Exception e)
                        {
                            Console.WriteLine("Exception during compiling " + complist + ": " + lineno);
                            Console.WriteLine(e.ToString());
                        }
                    }
                }

                using (FileStream fs = new FileStream(ctx.OutputFilename, FileMode.Create)) {
                    try {
                    DesigntimeLicenseContextSerializer.Serialize(fs, target.ToUpper(CultureInfo.InvariantCulture), ctx);
                    } catch {}
                    if (fs.Length == 0) // older mono does not support this, but when it does, we should use the proper version.
                        IntSerialize(fs, target.ToUpper(CultureInfo.InvariantCulture), ctx);
                }
                if (verbose)
                    Console.WriteLine("Saved to: "+ Path.GetFullPath(ctx.OutputFilename));
                return 0;
            } catch(Exception e){
                Console.WriteLine("Exception: "+e.ToString());
                return 1;
            }

        }


        private static void IntSerialize(Stream o,
                          string cryptoKey,
                          DesigntimeLicenseContext context)
        {
            Object[] lData = new Object[2];
            lData[0] = cryptoKey;
            Hashtable lNewTable = new Hashtable();
            FieldInfo fi =
                typeof(DesigntimeLicenseContext).GetField("savedLicenseKeys", BindingFlags.NonPublic | BindingFlags.Instance) ??
                typeof(DesigntimeLicenseContext).GetField("keys", BindingFlags.NonPublic | BindingFlags.Instance)
                ;
            Hashtable lOrgTable = (Hashtable)fi.GetValue(context);
            foreach (DictionaryEntry et in lOrgTable)
            {
                if (et.Key is string)
                    lNewTable.Add(et.Key, et.Value);
                else
                    lNewTable.Add(((Type)et.Key).AssemblyQualifiedName, et.Value);
            }
            lData[1] = lNewTable;

            BinaryFormatter lFormatter = new BinaryFormatter();
            lFormatter.Serialize(o, lData);

        }
        static Dictionary<string, Assembly> loadedAssemblies = new Dictionary<string, Assembly>();

        static bool CompareAssemblyName(string s1, string s2)
        {
            s1 = s1.ToLowerInvariant().Replace(" ", "");
            s2 = s2.ToLowerInvariant().Replace(" ", "");
            return s1 == s2;
        }

        static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            string[] lArgs = args.Name.Split(',');
            string lName = lArgs[0].Trim();
            if (loadedAssemblies.ContainsKey(args.Name))
            {
                return loadedAssemblies[args.Name];
            }
            for (int i = 0; i < privatePaths.Count; i++)
            {
                string sPath = Path.Combine(privatePaths[i].ToString(), lName);
                if (File.Exists(sPath + ".dll"))
                    sPath += ".dll";
                else if (File.Exists(sPath + ".DLL"))
                    sPath += ".DLL";
                else if (File.Exists(sPath + ".exe"))
                    sPath += ".exe";
                else if (File.Exists(sPath + ".EXE"))
                    sPath += ".EXE";
                else
                    continue;
                AssemblyName an2 = AssemblyName.GetAssemblyName(sPath);
                if (CompareAssemblyName(an2.ToString(), args.Name) || (lArgs.Length == 1 && CompareAssemblyName(an2.Name, lName)))
                {
                    Assembly asm;
                    try
                    {
                        asm = Assembly.LoadFrom(sPath);
                    }
                    catch
                    {
                        asm = Assembly.LoadFile(sPath);
                    }
                    if (asm != null)
                    {
                        loadedAssemblies.Add(args.Name, asm);
                        return asm;
                    }
                }
            }
            throw new Exception("Unable to find assembly "+args.Name);
        }
    }
}
