using MarketData;
using MarketData.Utils;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tor;
using Tor.Config;
using MarketData.Utils;
using Utility=MarketData.Utils.Utility;

namespace TorWebClient
{
  public class Program
  {
    private static readonly int TIMER_INTERVAL_MINUTES=2;
    static readonly string agentName="Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko";

    [DllImport("user32.dll")]
    static extern bool SetProcessDPIAware();

    [DllImport("wininet.dll",SetLastError=true,CharSet=CharSet.Auto)]
    static extern IntPtr InternetOpen(string lpszAgent,int dwAccessType,string lpszProxyName,string lpszProxyBypass,int dwFlags);

    [DllImport("wininet.dll",SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool InternetCloseHandle(IntPtr hInternet);

    [DllImport("wininet.dll",CharSet=CharSet.Ansi,SetLastError=true)]
    static extern bool InternetSetOption(IntPtr hInternet,INTERNET_OPTION dwOption,IntPtr lpBuffer,int lpdwBufferLength);

    [DllImport("wininet.dll",CharSet=CharSet.Ansi,SetLastError=true)]
    static extern bool InternetQueryOptionList(IntPtr handle,INTERNET_OPTION optionFlag,ref INTERNET_PER_CONN_OPTION_LIST optionList,ref int size);

    [DllImport("Kernel32")]
    public static extern bool SetConsoleCtrlHandler(HandlerRoutine handler,bool add);

    [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Ansi)]
    struct INTERNET_PER_CONN_OPTION_LIST
    {
      public int Size;
      public IntPtr Connection;
      public int OptionCount;
      public int OptionError;
      public IntPtr pOptions;
    }

    enum INTERNET_OPTION
    {
      INTERNET_OPTION_PER_CONNECTION_OPTION=75,
      INTERNET_OPTION_SETTINGS_CHANGED=39,
      INTERNET_OPTION_REFRESH=37
    }

    enum INTERNET_PER_CONN_OPTIONENUM
    {
      INTERNET_PER_CONN_FLAGS=1,
      INTERNET_PER_CONN_PROXY_SERVER=2,
      INTERNET_PER_CONN_PROXY_BYPASS=3,
      INTERNET_PER_CONN_AUTOCONFIG_URL=4,
      INTERNET_PER_CONN_AUTODISCOVERY_FLAGS=5,
      INTERNET_PER_CONN_AUTOCONFIG_SECONDARY_URL=6,
      INTERNET_PER_CONN_AUTOCONFIG_RELOAD_DELAY_MINS=7,
      INTERNET_PER_CONN_AUTOCONFIG_LAST_DETECT_TIME=8,
      INTERNET_PER_CONN_AUTOCONFIG_LAST_DETECT_URL=9,
      INTERNET_PER_CONN_FLAGS_UI=10
    }

    const int INTERNET_OPEN_TYPE_DIRECT=1;
    const int INTERNET_OPEN_TYPE_PRECONFIG=0;

    enum INTERNET_OPTION_PER_CONN_FLAGS
    {
      PROXY_TYPE_DIRECT=0x00000001,   // direct to net
      PROXY_TYPE_PROXY=0x00000002,   // via named proxy
      PROXY_TYPE_AUTO_PROXY_URL=0x00000004,   // autoproxy URL
      PROXY_TYPE_AUTO_DETECT=0x00000008   // use autoproxy detection
    }

    [StructLayout(LayoutKind.Explicit)]
    struct INTERNET_PER_CONN_OPTION_OPTIONUNION
    {
      [FieldOffset(0)]
      public int dwValue;
      [FieldOffset(0)]
      public IntPtr pszValue;
      [FieldOffset(0)]
      public FILETIME ftValue;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct INTERNET_PER_CONN_OPTION
    {
      public int dwOption;
      public INTERNET_PER_CONN_OPTION_OPTIONUNION value;
    }
    private static Program instance=null;
    private RouterCollection allRouters;
    private volatile bool closing;
    private CircuitCollection circuits;
    private Client client;
    private Timer dispatcherTimer;

    private Program()
    {
      closing=false;
      dispatcherTimer=new Timer(new TimerCallback(OnDispatcherTimerTick),null,0,(TIMER_INTERVAL_MINUTES*60*1000));      // every minute
    }
    public ManualResetEvent ResetEvent{get;set;}
    public static Program GetInstance()
    {
      lock(typeof(Program))
      {
        if(null==instance)instance=new Program();
        return instance;
      }
    }
    private void OnDispatcherTimerTick(object sender)
    {
      lock(this)
      {
        if(null==client) return;
        MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} DispatcherTimerTick : Total Uploaded: {1}, Total Downloaded: {2}",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now),client.Status.TotalBytesUploaded.ToString(),client.Status.TotalBytesDownloaded.ToString()));
        MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} DispatcherTimerTick : Cleaning circuits",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now)));
        client.Controller.CleanCircuits();
      }
    }
    private void OnClientBandwidthChanged(object sender,BandwidthEventArgs e)
    {
      lock(this)
      {
        //MDTrace.WriteLine(LogLevel.DEBUG,"OnClientBandwidthChanged");
        //if(closing) return;
      }
    }
    private void OnClientCircuitsChanged(object sender,EventArgs e)
    {
      lock(this)
      {
        //MDTrace.WriteLine(LogLevel.DEBUG,"OnClientCircuitsChanged");
        //circuits=client.Status.Circuits;
        //if(closing) return;
      }
    }
    private void OnClientConnectionsChanged(object sender,EventArgs e)
    {
      lock(this)
      {
      //MDTrace.WriteLine(LogLevel.DEBUG,"OnClientConnectionsChanged");
      //if(closing) return;
      }
    }
    private void OnClientStreamsChanged(object sender,EventArgs e)
    {
      lock(this)
      {
        //MDTrace.WriteLine(LogLevel.DEBUG,"OnClientStreamsChanged");
        //if(closing) return;
      }
    }
    private void OnClientShutdown(object sender,EventArgs e)
    {
      lock(this)
      {
        MDTrace.WriteLine(LogLevel.DEBUG,"OnClientStreamsChanged");
        if(!closing)
        {
          MDTrace.WriteLine(LogLevel.DEBUG,"OnClientShutdown : The tor client has been terminated without warning");
          return;
        }
        client=null;
      }
    }
    public void Start()
    {
      lock(this)
      {
        Process[] previous=Process.GetProcessesByName("tor");
        if(previous!=null&&previous.Length>0)
        {
          MDTrace.WriteLine(LogLevel.DEBUG,"Killing previous tor instances..");
          foreach(Process process in previous) process.Kill();
        }
        MDTrace.WriteLine(LogLevel.DEBUG,"Creating the tor client..");

        ClientCreateParams createParams=new ClientCreateParams();
        createParams.ConfigurationFile=ConfigurationManager.AppSettings["torConfigurationFile"];
        createParams.ControlPassword=ConfigurationManager.AppSettings["torControlPassword"];
        createParams.ControlPort=Convert.ToInt32(ConfigurationManager.AppSettings["torControlPort"]);
        createParams.DefaultConfigurationFile=ConfigurationManager.AppSettings["torDefaultConfigurationFile"];
        createParams.Path=ConfigurationManager.AppSettings["torPath"];
        createParams.SetConfig(ConfigurationNames.AvoidDiskWrites,true);
        createParams.SetConfig(ConfigurationNames.GeoIPFile,Path.Combine(Environment.CurrentDirectory,@"Tor\Data\Tor\geoip"));
        createParams.SetConfig(ConfigurationNames.GeoIPv6File,Path.Combine(Environment.CurrentDirectory,@"Tor\Data\Tor\geoip6"));
        createParams.SetConfig(ConfigurationNames.ExitNodes,"{US}");

        client=Client.Create(createParams);
        client.Configuration.Save(); 

        if(!client.IsRunning)
        {
          MDTrace.WriteLine(LogLevel.DEBUG,"The tor client could not be created");
          return;
        }

        if(!client.Proxy.IsRunning)
        {
          MDTrace.WriteLine(LogLevel.DEBUG,"The proxy is not running");
          return;
        }
        client.Status.BandwidthChanged+=OnClientBandwidthChanged;
        client.Status.CircuitsChanged+=OnClientCircuitsChanged;
        client.Status.ORConnectionsChanged+=OnClientConnectionsChanged;
        client.Status.StreamsChanged+=OnClientStreamsChanged;
        client.Shutdown+=new EventHandler(OnClientShutdown);
        Console.WriteLine(String.Format("127.0.0.1:{0}",client.Proxy.Port));
        if(!Program.SetConnectionProxy(string.Format("127.0.0.1:{0}",client.Proxy.Port)))
        {
          MDTrace.WriteLine(LogLevel.DEBUG,"The application could not set the default connection proxy. The browser control is not using the tor service as a proxy!");
        }

        allRouters=client.Status.GetAllRouters();
        if(null==allRouters)
        {
          MDTrace.WriteLine(LogLevel.DEBUG,"Could not download routers");
        }
      }
    }
    public void Stop()
    {
      lock(this)
      {
        MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} Disposing client.",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now)));
        client.Dispose();
      }
    }
    private String[] GetRouterNicknames(List<Router> routers)
    {
      lock(this)
      {
        List<String> nicknames=new List<String>();
        foreach(Router router in routers)
        {
          nicknames.Add(router.Nickname);
        }
        return nicknames.ToArray<String>();
      }
    }
    public void DisplayRouters(RouterCollection routers)
    {
      lock(this)
      {
        MDTrace.WriteLine(LogLevel.DEBUG,"Nickname,Identity,IP Address,Digest,Published,Bandwidth,Country,Flags");
        foreach(Router router in routers)
        {
          string countryCode = client.Status.GetCountryCode(router);
          StringBuilder sb=new StringBuilder();
          sb.Append(router.Nickname).Append(",");
          sb.Append(router.Identity).Append(",");
          sb.Append(router.IPAddress).Append(",");
          sb.Append(router.Digest).Append(",");
          sb.Append((router.Publication == DateTime.MinValue ? "Unknown" : Convert.ToString(router.Publication))).Append(",");
          sb.Append(router.Bandwidth).Append(",");
          sb.Append(countryCode).Append(",");
          sb.Append(router.Flags);
          MDTrace.WriteLine(LogLevel.DEBUG,sb.ToString());
        }
      }
    }
    public void DisplayRouters(List<Router> routers)
    {
      lock(this)
      {
        MDTrace.WriteLine(LogLevel.DEBUG,"Nickname,Identity,IP Address,Digest,Published,Bandwidth,Country,Flags");
        foreach(Router router in routers)
        {
          string countryCode=client.Status.GetCountryCode(router);
          StringBuilder sb=new StringBuilder();
          sb.Append(router.Nickname).Append(",");
          sb.Append(router.Identity).Append(",");
          sb.Append(router.IPAddress).Append(",");
          sb.Append(router.Digest).Append(",");
          sb.Append((router.Publication==DateTime.MinValue?"Unknown":Convert.ToString(router.Publication))).Append(",");
          sb.Append(router.Bandwidth).Append(",");
          sb.Append(countryCode).Append(",");
          sb.Append(router.Flags);
          MDTrace.WriteLine(LogLevel.DEBUG,sb.ToString());
        }
      }
    }
    public static bool SetConnectionProxy(string proxyServer)
    {
      IntPtr hInternet=InternetOpen(agentName,INTERNET_OPEN_TYPE_DIRECT,null,null,0);

      INTERNET_PER_CONN_OPTION[] options=new INTERNET_PER_CONN_OPTION[2];
      options[0]=new INTERNET_PER_CONN_OPTION();
      options[0].dwOption=(int)INTERNET_PER_CONN_OPTIONENUM.INTERNET_PER_CONN_FLAGS;
      options[0].value.dwValue=(int)INTERNET_OPTION_PER_CONN_FLAGS.PROXY_TYPE_PROXY;

      options[1]=new INTERNET_PER_CONN_OPTION();
      options[1].dwOption=(int)INTERNET_PER_CONN_OPTIONENUM.INTERNET_PER_CONN_PROXY_SERVER;
      options[1].value.pszValue=Marshal.StringToHGlobalAnsi(proxyServer);

      IntPtr buffer=Marshal.AllocCoTaskMem(Marshal.SizeOf(options[0])+Marshal.SizeOf(options[1]));
      IntPtr current=buffer;

      for(int i=0;i<options.Length;i++)
      {
        Marshal.StructureToPtr(options[i],current,false);
        current=(IntPtr)((int)current+Marshal.SizeOf(options[i]));
      }

      INTERNET_PER_CONN_OPTION_LIST optionList=new INTERNET_PER_CONN_OPTION_LIST();
      optionList.pOptions=buffer;
      optionList.Size=Marshal.SizeOf(optionList);
      optionList.Connection=IntPtr.Zero;
      optionList.OptionCount=options.Length;
      optionList.OptionError=0;

      int size=Marshal.SizeOf(optionList);
      IntPtr intPtrStruct=Marshal.AllocCoTaskMem(size);
      Marshal.StructureToPtr(optionList,intPtrStruct,true);

      bool bReturn=InternetSetOption(hInternet,INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION,intPtrStruct,size);

      Marshal.FreeCoTaskMem(buffer);
      Marshal.FreeCoTaskMem(intPtrStruct);

      InternetCloseHandle(hInternet);

      return bReturn;
    }
    public delegate bool HandlerRoutine(CtrlTypes handler);
    public enum CtrlTypes { CTRL_C_EVENT=0, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT=5, CTRL_SHUTDOWN_EVENT }
    private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
    {
      switch(ctrlType)
      {
        case CtrlTypes.CTRL_C_EVENT:
          Program.GetInstance().Stop();
          MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} Received CTRL_C_EVENT.",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now)));
          Program.GetInstance().ResetEvent.Set();
          break;
        case CtrlTypes.CTRL_BREAK_EVENT:
          Program.GetInstance().Stop();
          MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} Received CTRL_BREAK_EVENT.",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now)));
          Program.GetInstance().ResetEvent.Set();
          break;
        case CtrlTypes.CTRL_CLOSE_EVENT:
          Program.GetInstance().Stop();
          MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} Received CTRL_CLOSE_EVENT.",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now)));
          Program.GetInstance().ResetEvent.Set();
          break;
        case CtrlTypes.CTRL_LOGOFF_EVENT:
          Program.GetInstance().Stop();
          MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} Received CTRL_LOGOFF_EVENT.",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now)));
          Program.GetInstance().ResetEvent.Set();
          break;
        case CtrlTypes.CTRL_SHUTDOWN_EVENT:
          Program.GetInstance().Stop();
          MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} Received CTRL_SHUTDOWN_EVENT.",Utility.DateTimeToStringMMSDDSYYYYHHMMSS(DateTime.Now)));
          Program.GetInstance().ResetEvent.Set();
          break;
      }
      return true;
    }
    static void Main(string[] args)
    {
      try{Program.SetConsoleCtrlHandler(new HandlerRoutine(Program.ConsoleCtrlCheck),true);}
      catch(Exception exception){Console.WriteLine(exception.ToString());}
      if(!Utility.IsAdministrator())
      {
        Console.WriteLine("TorProxy needs to be under administrator account.  Press any key to exit.");
        Console.ReadKey();
        return;
      }
      MDTrace.LogLevel=LogLevel.DEBUG;
      String strLogFile="torwebclient.log";
      Utility.DeleteFile(strLogFile);
      Trace.Listeners.Add(new TextWriterTraceListener(strLogFile));
//      Program program=new Program();

      ManualResetEvent[] resetEvents=new ManualResetEvent[1];
      for(int index=0;index<resetEvents.Length;index++) resetEvents[index]=new ManualResetEvent(false);
      resetEvents[0].Reset();
      ThreadPool.QueueUserWorkItem(delegate
      {
        Program.GetInstance().Start();
        Program.GetInstance().ResetEvent=resetEvents[0];
//        program.Start();
        MDTrace.WriteLine(LogLevel.DEBUG,"Running...");
      });
      MDTrace.WriteLine(LogLevel.DEBUG,"Waiting for terminal event...");
      WaitHandle.WaitAll(resetEvents);
      MDTrace.WriteLine(LogLevel.DEBUG,"Terminated...");
    }
  }
}
