using System;
using System.Collections;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Serialization;

using ByteFX.Data.MySqlClient;

using SourceSafeTypeLib;

namespace VSSMantisService
{
  /// <summary>
  /// Integrates Visual Source Safe with Mantis Bugtracker.
  /// </summary>
  public class Integrator
  {
    /// <summary>
    /// Reads Visual Source Safe information from the application
    /// configuration and constructs an Integrator object.
    /// </summary>
    public Integrator()
    {
      user_ = ConfigurationSettings.AppSettings["username"];
      password_ = ConfigurationSettings.AppSettings["password"];
      dir_ = ConfigurationSettings.AppSettings["srcsafeini"];

      if (!EventLog.Exists(@"VSSMantis"))
        EventLog.CreateEventSource(@"VSSMantis", @"VSSMantis", @".");

      eventlog_ = new EventLog();
      eventlog_.Source = @"VSSMantis";
    }

    /// <summary>
    /// Manages timely destruction of the event log, "VSSMantis".
    /// </summary>
    ~Integrator()
    {
    }

    /// <summary>
    /// Extracts bug-related checkin comments from Visual Source Safe
    /// and adds it as bugnotes to Mantis.
    /// </summary>
    public void Run()
    {
      start_ = DateTime.Now;
      Unpersist();

      eventlog_.WriteEntry(@"Commencing run.", EventLogEntryType.Information);

      database_.Open(dir_, user_, password_);

      IVSSItem item = database_.get_VSSItem(ConfigurationSettings.AppSettings["rootdir"], false);
      if (item.Type != 0)
      {
        eventlog_.WriteEntry(@"rootdir not valid directory", EventLogEntryType.Error);
        return;
      }

      ArrayList bugitems = ProcessDir(item);
      foreach (VersionInfo vinfo in bugitems)
      {
        try
        {
          UpdateMantisDB(vinfo.bugitem, vinfo.version);
        }
        catch (Exception ex)
        {
          string error = string.Format(@"Error occurred updating mantis tables - this entry has been lost: {0} version: {1}\r\nThe error was: {2}\r\nResuming with remainding items.", vinfo.version.VSSItem.Spec, vinfo.version.VersionNumber.ToString(), ex.Message);
          eventlog_.WriteEntry(error, EventLogEntryType.Error);
        }
      }

      eventlog_.WriteEntry(@"Terminating run.", EventLogEntryType.Information);

      Persist();
    }

    private void Persist()
    {
      try
      {
        using (StreamWriter sw = new StreamWriter(@"lastrun.xml", false, Encoding.UTF8))
        {
          XmlSerializer xs = new XmlSerializer(typeof(DateTime));
          xs.Serialize(sw, start_);
        }
      }
      catch (Exception)
      {

      }
      finally
      {
        since_ = start_;
      }
    }

    private void Unpersist()
    {
      try
      {
        using (StreamReader sr = new StreamReader(@"lastrun.xml", Encoding.UTF8))
        {
          XmlSerializer xs = new XmlSerializer(typeof(DateTime));
          since_ = (DateTime)xs.Deserialize(sr.BaseStream);
        }
      }
      catch (Exception)
      {
        since_ = new DateTime(0);
      }
    }

    private ArrayList ProcessDir(IVSSItem item)
    {
      ArrayList al = new ArrayList();
      foreach (IVSSItem child in item.get_Items(false))
      {
        if (child.Type == 0)
        {
          ArrayList tmp = ProcessDir(child);
          al.AddRange(tmp);
        }
        else
        {
          ArrayList tmp = ProcessFile(child);
          al.AddRange(tmp);
        }
      }

      return al;
    }

    private ArrayList ProcessFile(IVSSItem item)
    {
      Stack versions = new Stack();

      foreach (IVSSVersion version in item.get_Versions(0))
      {
        if (version.Date.CompareTo(since_) >= 0)
        {
          if (version.Date.CompareTo(start_) < 0)
            versions.Push(version);
        }
      }

      ArrayList al = new ArrayList();
      while (versions.Count > 0)
      {
        IVSSVersion version = (IVSSVersion)versions.Pop();

        BugItem[] bugitems = GetBugItems(version);
        foreach (BugItem bugitem in bugitems)
          al.Add(new VersionInfo(bugitem, version));
      }

      return al;
    }

    private struct VersionInfo
    {
      public BugItem bugitem;
      public IVSSVersion version;

      public VersionInfo(BugItem bugitem, IVSSVersion version)
      {
        this.bugitem = bugitem;
        this.version = version;
      }
    }

    private BugItem[] GetBugItems(IVSSVersion version)
    {
      return GetBugItems(version, new DateTime(0));
    }

    private BugItem[] GetBugItems(IVSSVersion version, DateTime since)
    {
      if (version == null)
        throw new ArgumentException("Parameter must not be null.", "version");

      if (!since.Equals(null) && version.Date.CompareTo(since) < 0)
        return null;

      string[] lines = version.Comment.Split(new char[]{'\n'});

      ArrayList items = new ArrayList();

      for (int i = 0; i < lines.Length; ++i)
      {
        Match match = Regex.Match(lines[i], @"^\[#?(\d+)\]?(.*)", RegexOptions.Compiled);

        if (match.Success)
        {
          string id = match.Groups[1].Value;
          string note = match.Groups[2].Value;

          items.Add(new BugItem(id, note));
        }
      }

      BugItem[] bugitems = new BugItem[items.Count];
      items.CopyTo(bugitems);

      return bugitems;
    }

    private struct BugItem
    {
      public string Id;
      public string Comment;

      public BugItem(string Id, string Comment)
      {
        this.Id = Id;
        this.Comment = Comment;
      }
    }

    private void UpdateMantisDB(BugItem item, IVSSVersion version)
    {
      using (MySqlConnection conn = new MySqlConnection(ConfigurationSettings.AppSettings["mantis"]))
      {
        conn.Open();

        MySqlTransaction trans = conn.BeginTransaction();

        // find user id
        MySqlCommand cmd_user = conn.CreateCommand();
        cmd_user.CommandText = @"SELECT id FROM mantis_user_table WHERE username = @username";
        cmd_user.Parameters.Add("@username", MySqlDbType.VarChar, 32).Value = version.Username;

        object oid = cmd_user.ExecuteScalar();

        if (oid == null)
        {
          eventlog_.WriteEntry(@"Unable to find user with username: " + version.Username + " for item: " + version.VSSItem.Spec, EventLogEntryType.FailureAudit);
          trans.Rollback();
          return;
        }

        uint id = (uint)oid;

        // ensure bug exists
        MySqlCommand cmd_bug_cnt = conn.CreateCommand();
        cmd_bug_cnt.CommandText = @"SELECT COUNT(id) FROM mantis_bug_table WHERE id = @id";
        cmd_bug_cnt.Parameters.Add(@"id", MySqlDbType.Int, 7).Value = item.Id;

        int ct = (int)cmd_bug_cnt.ExecuteScalar();

        if (ct == 0)
        {
          eventlog_.WriteEntry(@"Unable to find bug item with id: " + item.Id + " for item: " + version.VSSItem.Spec, EventLogEntryType.FailureAudit);
          trans.Rollback();
          return;
        }

        // add bugnote text
        string comment_data = string.Format("Tracking: {0}\r\n"
          + "Version: {3}\r\n"
          + "Checked in: {1}\r\n"
          + "Comment: {2}",
          version.VSSItem.Spec,
          version.Date.ToString(),
          item.Comment,
          version.VersionNumber);

        MySqlCommand cmd_bugnote_text = conn.CreateCommand();
        cmd_bugnote_text.CommandText = @"INSERT INTO mantis_bugnote_text_table (note) VALUES (@note)";
        cmd_bugnote_text.Parameters.Add(@"note", MySqlDbType.Blob).Value = comment_data;

        int aff = cmd_bugnote_text.ExecuteNonQuery();
        if (aff != 1)
        {
          eventlog_.WriteEntry(@"Error inserting bug comment, no rows inserted for item " + version.VSSItem.Spec + ".", EventLogEntryType.Error);
          trans.Rollback();
          return;
        }

        // pick out bugnote text id
        MySqlCommand cmd_bugnote_text_id = conn.CreateCommand();
        cmd_bugnote_text_id.CommandText = @"SELECT MAX(id) FROM mantis_bugnote_text_table";

        uint btid = (uint)cmd_bugnote_text_id.ExecuteScalar();

        // update bugnote table
        MySqlCommand cmd_bugnote = conn.CreateCommand();
        cmd_bugnote.CommandText = @"INSERT INTO mantis_bugnote_table (bug_id, reporter_id, bugnote_text_id, view_state, date_submitted, last_modified) VALUES (@bug_id, @reporter_id, @bugnote_text_id, @view_state, @date_submitted, @last_modified)";
        cmd_bugnote.Parameters.Add(@"bug_id", MySqlDbType.Int, 7).Value = item.Id;
        cmd_bugnote.Parameters.Add(@"reporter_id", MySqlDbType.Int, 7).Value = id;
        cmd_bugnote.Parameters.Add(@"bugnote_text_id", MySqlDbType.Int, 7).Value = btid;
        cmd_bugnote.Parameters.Add(@"view_state", MySqlDbType.Int, 2).Value = 10;
        cmd_bugnote.Parameters.Add(@"date_submitted", MySqlDbType.Datetime).Value = DateTime.Now;
        cmd_bugnote.Parameters.Add(@"last_modified", MySqlDbType.Datetime).Value = DateTime.Now;

        aff = cmd_bugnote.ExecuteNonQuery();

        if (aff != 1)
        {
          eventlog_.WriteEntry(@"Error adding bugnote info for item: " + version.VSSItem.Spec, EventLogEntryType.Error);
          trans.Rollback();
          return;
        }

        // get bugnote table id
        MySqlCommand cmd_bugnote_id = conn.CreateCommand();
        cmd_bugnote_id.CommandText = @"SELECT MAX(id) FROM mantis_bugnote_table";

        uint bugnoteid = (uint)cmd_bugnote_id.ExecuteScalar();

        // update history table
        MySqlCommand cmd_hist = conn.CreateCommand();
        cmd_hist.CommandText = @"INSERT INTO mantis_bug_history_table (user_id, bug_id, date_modified, field_name, old_value, new_value, type) VALUES (@user_id, @bug_id, @date_modified, '', @old_value, '', 2)";
        cmd_hist.Parameters.Add("@user_id", MySqlDbType.Int, 7).Value = id;
        cmd_hist.Parameters.Add("@bug_id", MySqlDbType.Int, 7).Value = item.Id;
        cmd_hist.Parameters.Add("@date_modified", MySqlDbType.Datetime).Value = DateTime.Now;
        cmd_hist.Parameters.Add("@old_value", MySqlDbType.VarChar, 128).Value = bugnoteid.ToString().PadLeft(7, '0');

        aff = cmd_hist.ExecuteNonQuery();

        if (aff != 1)
        {
          eventlog_.WriteEntry(@"Error writing history information for bug for item: " + version.VSSItem.Spec, EventLogEntryType.Error);
          trans.Rollback();
          return;
        }

        trans.Commit();
      }
    }

    private IVSSDatabase database_ = new VSSDatabase();

    private string user_;
    private string dir_;
    private string password_;

    private DateTime start_;
    private DateTime since_;

    private System.Diagnostics.EventLog eventlog_;
  }
}