using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Xunit;

namespace xUnitRevitUtils
{
  /// <summary>
  /// Utility class with methods and properties used by the xUnit Revit plugin
  /// </summary>
  public static class xru
  {
    public static UIApplication Uiapp { get; set; }
    private static IList<Action> Queue { get; set; }
    private static ExternalEvent EventHandler { get; set; }
    public static SynchronizationContext UiContext { get; set; }
    public static void Initialize(UIApplication uiapp, SynchronizationContext uiContext, ExternalEvent eventHandler, IList<Action> queue)
    {
      Uiapp = uiapp;
      UiContext = uiContext;
      EventHandler = eventHandler;
      Queue = queue;
    }

    #region utility methods

    /// <summary>
    /// Returns the selected elements in the active document
    /// </summary>
    /// <returns></returns>
    public static IList<Element> GetActiveSelection()
    {
      Assert.NotNull(Uiapp);

      if (Uiapp.ActiveUIDocument != null)
        return Uiapp.ActiveUIDocument.Selection.GetElementIds().Select(x => Uiapp.ActiveUIDocument.Document.GetElement(x)).ToList();
      return new List<Element>();
    }
    /// <summary>
    /// Opens and activates a document if not open already
    /// </summary>
    /// <param name="filePath">Path to the file to open</param>
    public static Document OpenDoc(string filePath)
    {
      Assert.NotNull(Uiapp);
      Document doc = null;
      //OpenAndActivateDocument only works if run from the current context
      UiContext.Send(x => doc = Uiapp.OpenAndActivateDocument(filePath).Document, null);
      Assert.NotNull(doc);
      return doc;
    }
    /// <summary>
    /// Closes the provided Revit document
    /// </summary>
    /// <param name="doc">Revit document to be closed</param>
    /// <param name="saveChanges">If true, saves changes to document before closing</param>
    /// <returns>
    /// Bool indicating whether or not the document was successfully closed
    /// </returns>
    public static bool CloseDoc(Document doc, bool saveChanges = false)
    {
      if (doc == null)
        return false;

      var result = false;
      UiContext.Send(x => result = doc.Close(saveChanges), null);
      return result;
    }

    /// <summary>
    /// Creates a new empty document
    /// </summary>
    /// <param name="templatePath">Path to the project template</param>
    /// <param name="filePath">Path where to save the new doc</param>
    /// <param name="overwrite">If true overwrites existing files with same name</param>
    /// <returns></returns>
    public static Document CreateNewDoc(string templatePath, string filePath, bool overwrite = true)
    {
      Assert.NotNull(Uiapp);
      Document doc = null;

      try
      {
        if (overwrite && File.Exists(filePath))
          File.Delete(filePath);
      }
      catch
      {
      }

      //OpenAndActivateDocument only works if run from the current context
      UiContext.Send(x =>
      {
        //if already open, just use it
        if (!File.Exists(filePath))
        {
          doc = Uiapp.Application.NewProjectDocument(templatePath);
          doc.SaveAs(filePath);
          doc.Close();
        }

        doc = Uiapp.OpenAndActivateDocument(filePath).Document;
      }
      , null);
      Assert.NotNull(doc);
      return doc;
    }

    /// <summary>
    /// Runs an Action in a Revit transaction, uses TaskCompletionSource to communicate when done
    /// </summary>
    /// <param name="action">Action to run</param>
    /// <param name="doc">Revit Document</param>
    /// <param name="transactionName">Transaction Name</param>
    /// <param name="ignoreWarnings">Enable to swallow all warnings generated by the transaction and prevent them from being raised within Revit</param>
    /// <returns></returns>
    public static Task RunInTransaction(Action action, Document doc, string transactionName = "transaction", bool ignoreWarnings = false)
    {
      var tcs = new TaskCompletionSource<string>();
      Queue.Add(new Action(() =>
      {
        try
        {
          using var transaction = new Transaction(doc, transactionName);
          transaction.Start();

          if (ignoreWarnings)
          {
            using var options = transaction.GetFailureHandlingOptions();
            options.SetFailuresPreprocessor(new IgnoreAllWarnings());
            transaction.SetFailureHandlingOptions(options);
          }

          action.Invoke();
          transaction.Commit();
        }
        catch (Exception e)
        {
          tcs.TrySetException(e);
        }
        tcs.TrySetResult("");
      }));

      EventHandler.Raise();

      return tcs.Task;
    }

    /// <summary>
    /// Runs an Action, uses TaskCompletionSource to communicate when done
    /// </summary>
    /// <param name="action">Action to run</param>
    /// <param name="doc">Revit Document</param>
    /// <returns></returns>
    public static Task Run(Action action, Document doc)
    {
      var tcs = new TaskCompletionSource<string>();
      Queue.Add(new Action(() =>
      {
        try
        {
          action.Invoke();
        }
        catch (Exception e)
        {
          tcs.TrySetException(e);
        }
        tcs.TrySetResult("");
      }));

      EventHandler.Raise();

      return tcs.Task;
    }

    /// <summary>
    /// A failures preprocesser that clears any failures that occur within a transaction
    /// </summary>
    internal class IgnoreAllWarnings : IFailuresPreprocessor
    {
      public FailureProcessingResult PreprocessFailures(FailuresAccessor failuresAccessor)
      {
        var failList = failuresAccessor.GetFailureMessages();

        foreach (FailureMessageAccessor failure in failList)
        {
          failuresAccessor.DeleteWarning(failure);
        }

        return FailureProcessingResult.Continue;
      }
    }
    #endregion
  }
}
