using System.Runtime.InteropServices;
using GH_IO.Serialization;
using Grasshopper.Kernel;
using Speckle.Connectors.GrasshopperShared.Parameters;
using Speckle.Connectors.GrasshopperShared.Properties;

namespace Speckle.Connectors.GrasshopperShared.Components.Objects;

/// <summary>
/// CreateSpeckleProperties passthrough component by key value pairs
/// </summary>
[Guid("FED2298C-0D2B-4868-94B5-B8D17F9385A5")]
public class SpecklePropertiesPassthrough : SpeckleSolveInstance
{
  public override Guid ComponentGuid => GetType().GUID;
  protected override Bitmap Icon => Resources.speckle_properties_properties;
  public override GH_Exposure Exposure => GH_Exposure.quarternary;

  public SpecklePropertiesPassthrough()
    : base(
      "Speckle Properties",
      "SP",
      "Creates or modifies a set of properties for Speckle objects by keyvalue",
      ComponentCategories.PRIMARY_RIBBON,
      ComponentCategories.OBJECTS
    )
  {
    Message = Mode.ToString();
  }

  private enum PropertyMode
  {
    Merge,
    Replace,
    Remove,
    Update, // pre rewording (cnx-3177), keeping for scripts with settings saved
    Overwrite // pre rewording (cnx-3177), keeping for scripts with settings saved
  }

  private PropertyMode _mode = PropertyMode.Update;
  private PropertyMode Mode
  {
    get => _mode;
    set
    {
      // auto-migrate legacy modes to new modes
      if (value == PropertyMode.Update)
      {
        value = PropertyMode.Merge;
      }

      if (value == PropertyMode.Overwrite)
      {
        value = PropertyMode.Replace;
      }

      if (_mode != value)
      {
        _mode = value;
        Message = Mode.ToString();
        ExpireSolution(true);
      }
    }
  }

  protected override void RegisterInputParams(GH_InputParamManager pManager)
  {
    pManager.AddParameter(new SpecklePropertyGroupParam(), "Properties", "P", "Input properties", GH_ParamAccess.item);
    Params.Input[0].Optional = true;

    pManager.AddTextParameter("Keys", "k", "Keys of all properties", GH_ParamAccess.list);
    Params.Input[1].Optional = true;

    pManager.AddGenericParameter(
      "Values",
      "v",
      "Values of all properties. Accepts text, number, bool, null, or other properties as inputs.",
      GH_ParamAccess.list
    );
    Params.Input[2].Optional = true;
  }

  protected override void RegisterOutputParams(GH_OutputParamManager pManager)
  {
    pManager.AddParameter(new SpecklePropertyGroupParam(), "Properties", "P", "Properties", GH_ParamAccess.item);
    pManager.AddTextParameter("Keys", "k", "Keys of all properties", GH_ParamAccess.list);
    pManager.AddGenericParameter("Values", "v", "Values of all properties", GH_ParamAccess.list);
  }

  protected override void SolveInstance(IGH_DataAccess da)
  {
    SpecklePropertyGroupGoo? inputProperties = null;
    da.GetData(0, ref inputProperties);

    List<string> inputKeys = new();
    bool hasKeys = da.GetDataList(1, inputKeys);

    List<object?> inputValues = new();
    bool hasValues = da.GetDataList(2, inputValues);

    // if no props or keyvalues were input, create empty props and return
    if (inputProperties == null && !hasKeys)
    {
      SpecklePropertyGroupGoo emptyProps = new();
      da.SetData(0, emptyProps);
      da.SetDataList(1, emptyProps.Value.Keys);
      da.SetDataList(2, emptyProps.Value.Values);
      return;
    }

    // validate that keys and values are of valid length
    if ((Mode == PropertyMode.Update || Mode == PropertyMode.Overwrite) && inputKeys.Count != inputValues.Count)
    {
      AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Keys and values are mismatched in length");
      return;
    }

    if (Mode == PropertyMode.Remove && (inputKeys.Count == 0 || inputValues.Count > 0))
    {
      AddRuntimeMessage(GH_RuntimeMessageLevel.Warning, "Only input keys to remove");
      return;
    }

    // deep clone to prevent mutating upstream grasshopper data
    SpecklePropertyGroupGoo resultGoo =
      inputProperties is null || Mode == PropertyMode.Replace ? new SpecklePropertyGroupGoo() : inputProperties.Clone();

    // process keys and values
    if (hasKeys)
    {
      // check for duplicate keys
      var duplicates = inputKeys.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
      if (duplicates.Count != 0)
      {
        AddRuntimeMessage(
          GH_RuntimeMessageLevel.Error,
          $"Duplicate property keys found: {string.Join(", ", duplicates)}"
        );
        return;
      }

      // set key-value pairs
      for (int i = 0; i < inputKeys.Count; i++)
      {
        string key = inputKeys[i];
        object? value = Mode == PropertyMode.Remove ? null : inputValues[i];
        ISpecklePropertyGoo? convertedValue;
        switch (value)
        {
          case SpecklePropertyGroupGoo propGoo:
            convertedValue = propGoo;
            break;
          case null:
            convertedValue = new SpecklePropertyGoo();
            break;
          default:
            SpecklePropertyGoo propValue = new();
            if (!propValue.CastFrom(value))
            {
              AddRuntimeMessage(
                GH_RuntimeMessageLevel.Error,
                "Values contain an invalid data type. Only strings, numbers, booleans, planes, vectors, intervals, and other Speckle properties are supported."
              );
              return;
            }

            convertedValue = propValue;
            break;
        }

        switch (Mode)
        {
          case PropertyMode.Merge:
          case PropertyMode.Replace:
            resultGoo.SetValueByPath(key, convertedValue);
            break;
          case PropertyMode.Remove:
            resultGoo.RemoveValueByPath(key);
            break;
        }
      }
    }

    da.SetData(0, resultGoo);
    da.SetDataList(1, resultGoo.Value.Keys);
    da.SetDataList(2, resultGoo.Value.Values);
  }

  public override void AppendAdditionalMenuItems(ToolStripDropDown menu)
  {
    base.AppendAdditionalMenuItems(menu);

    Menu_AppendSeparator(menu); // modes section
    foreach (PropertyMode mode in Enum.GetValues(typeof(PropertyMode)))
    {
      // hide "legacy modes" (before cnx-3177 rewording) from the dropdown
      if (mode is PropertyMode.Update or PropertyMode.Overwrite)
      {
        continue;
      }

      var modeItem = Menu_AppendItem(menu, mode.ToString(), (_, _) => Mode = mode, true, mode == Mode);
      switch (mode)
      {
        case PropertyMode.Merge:
          modeItem.ToolTipText =
            @"Input key-value pairs will be merged with existing properties. Any existing keys will be updated with new values.";
          break;
        case PropertyMode.Replace:
          modeItem.ToolTipText = @"Existing properties will be cleared and replaced by input key-value pairs.";
          break;
        case PropertyMode.Remove:
          modeItem.ToolTipText = @"Existing key-value pairs that match the input keys will be removed from properties.";
          break;
      }
    }

    Menu_AppendSeparator(menu);
  }

  public override bool Write(GH_IWriter writer)
  {
    var result = base.Write(writer);
    writer.SetString("Mode", Mode.ToString());
    return result;
  }

  public override bool Read(GH_IReader reader)
  {
    var result = base.Read(reader);
    string mode = "";
    if (reader.TryGetString("Mode", ref mode))
    {
      if (Enum.TryParse(mode, out PropertyMode modeEnum))
      {
        Mode = modeEnum;
      }
    }

    return result;
  }
}
