defmodule CleanMixer.UI.ArchMapRendering.PlantUMLRenderer do
  alias CleanMixer.ArchMap
  alias CleanMixer.ArchMap.Component
  alias CleanMixer.ArchMap.Dependency
  alias CleanMixer.Metrics.MetricsMap

  alias CleanMixer.Metrics.ComponentMetrics.FanIn
  alias CleanMixer.Metrics.ComponentMetrics.FanOut
  alias CleanMixer.Metrics.ComponentMetrics.Instability
  alias CleanMixer.Metrics.ComponentMetrics.Stability
  alias CleanMixer.Metrics.ComponentMetrics.Abstractness
  alias CleanMixer.Metrics.ComponentMetrics.Distance
  alias CleanMixer.Metrics.ComponentMetrics.PublicFiles
  alias CleanMixer.Metrics.ComponentMetrics.AbstractIn
  alias CleanMixer.Metrics.ComponentMetrics.AbstractOut
  alias CleanMixer.Metrics.DependencyMetrics.Usage

  @spec render(ArchMap.t(), MetricsMap.t(Component.t()), MetricsMap.t(Dependency.t()), map) :: String.t()
  def render(arch_map, component_metrics, dependency_metrics, params) do
    [
      "@startuml",
      "skinparam backgroundColor<<dep>> Grey",
      "skinparam legend {\n FontSize 20\n }",
      "legend bottom left\n #{legend(params)} \n endlegend",
      format_components(arch_map.components, group_tag(params), component_metrics),
      Enum.map(arch_map.dependencies, &format_dependency(&1, component_metrics, dependency_metrics, params)),
      "@enduml"
    ]
    |> List.flatten()
    |> Enum.join("\n")
  end

  defp group_tag(%{group: true}), do: :group
  defp group_tag(_params), do: :group_tag_undefined

  defp format_components(components, group_tag, component_metrics) do
    components
    |> Enum.group_by(&get_in(&1.meta, [:tags, group_tag]))
    |> Enum.map(&format_component_group(&1, component_metrics))
  end

  defp format_component_group({_group_name = nil, components}, component_metrics) do
    Enum.map(components, &format_component(&1, component_metrics))
  end

  defp format_component_group({group_name, components}, component_metrics) do
    [
      ~s(package "#{group_name}" {),
      Enum.map(components, &format_component(&1, component_metrics)),
      "}"
    ]
  end

  defp legend(params) do
    main_legend = [
      "I = Instability = out / (in + out)",
      "S = Stability = 1 - I",
      "A = Abstractness = behaviours / total_modules",
      "D = Distance = |A+I-1|",
      "Tf = Total files",
      "Pf = Public files",
      "Ain = Abstract in",
      "Aout = Abstract out"
    ]

    full_legend =
      if params[:verbose] do
        [main_legend, "U = Usage = UsedFiles / Pf"]
      else
        main_legend
      end

    full_legend |> List.flatten() |> Enum.join("\n")
  end

  defp format_component(comp, metrics_map) do
    "rectangle #{sanitize(comp.name)} #{colour(comp)} [ =#{comp.name} \n\n #{component_desc(comp, metrics_map)} ]"
  end

  defp colour(comp) do
    if Component.hex_pack?(comp) do
      "#LightGrey"
    else
      ""
    end
  end

  defp component_desc(comp, metrics_map) do
    metrics = MetricsMap.metrics_of(metrics_map, comp)

    fan_in = metrics[FanIn]
    fan_out = metrics[FanOut]
    stability = format_metric(metrics[Stability])
    instability = format_metric(metrics[Instability])

    abstractness = format_metric(metrics[Abstractness])
    distance = format_metric(metrics[Distance])
    distance_sigmas = format_metric(MetricsMap.sigmas_count(metrics_map, comp, Distance))

    total_files = Enum.count(comp.files)
    public_files = metrics[PublicFiles]
    abstract_in = metrics[AbstractIn]
    abstract_out = metrics[AbstractOut]

    "In=#{fan_in} Out=#{fan_out} S=#{stability} (I=#{instability}) \n" <>
      "A=#{abstractness} D=#{distance} (#{distance_sigmas}σ) \n" <>
      "Tf=#{total_files} Pf=#{public_files} Ain=#{abstract_in} Aout=#{abstract_out}"
  end

  defp format_dependency(%Dependency{} = dep, component_metrics, dependency_metrics, params) do
    link = "[#{sanitize(dep.source.name)}] -[#{link_style(dep, component_metrics)}]-> [#{sanitize(dep.target.name)}]"

    if params[:verbose] do
      usage = dependency_metrics |> MetricsMap.metric(dep, Usage) |> format_metric()

      link <> " : " <> "U=#{usage}"
    else
      link
    end
  end

  defp link_style(%Dependency{} = dep, component_metrics) do
    source_stability = MetricsMap.metric(component_metrics, dep.source, Stability)
    target_stability = MetricsMap.metric(component_metrics, dep.target, Stability)

    if source_stability > target_stability do
      "#red,bold"
    else
      "#black"
    end
  end

  defp format_metric(value) do
    Float.round(value, 2)
  end

  defp sanitize(name) do
    name
    |> String.replace(" ", "_")
    |> String.replace("/", "_")
    |> String.replace("-", "_")
  end
end
