defmodule JSON.LD.Decoder do
  @moduledoc """
  A decoder for JSON-LD serializations to `RDF.Dataset`s.

  As for all decoders of `RDF.Serialization.Format`s, you normally won't use these
  functions directly, but via one of the `read_` functions on the `JSON.LD` format
  module or the generic `RDF.Serialization` module.
  """

  use RDF.Serialization.Decoder

  import JSON.LD.{NodeIdentifierMap, Utils}

  alias JSON.LD.{NodeIdentifierMap, Options}
  alias RDF.{BlankNode, Dataset, Graph, IRI, Literal, NS, Statement, XSD}

  @rdf_direction RDF.__base_iri__() <> "direction"
  @rdf_language RDF.__base_iri__() <> "language"

  @impl RDF.Serialization.Decoder
  @spec decode(String.t(), keyword) :: {:ok, Dataset.t() | Graph.t()} | {:error, any}
  def decode(content, opts \\ []) do
    with {:ok, json_ld_object} <- parse_json(content) do
      {:ok, to_rdf(json_ld_object, opts)}
    end
  end

  @spec to_rdf(JSON.LD.input(), Options.t() | Enum.t()) :: Dataset.t() | Graph.t()
  def to_rdf(input, options \\ %Options{}) do
    {:ok, node_id_map} = NodeIdentifierMap.start_link()

    options = Options.new(options)

    try do
      input
      |> JSON.LD.expand(%{options | ordered: false})
      |> JSON.LD.node_map(node_id_map)
      |> do_to_rdf(node_id_map, options)
    after
      NodeIdentifierMap.stop(node_id_map)
    end
  end

  defp do_to_rdf(node_map, node_id_map, options) do
    node_map
    |> Enum.sort_by(fn {graph_name, _} -> graph_name end)
    |> Enum.reduce(Dataset.new(), fn {graph_name, graph}, dataset ->
      # 1.1)
      if graph_name != "@default" and not well_formed_iri?(graph_name) and
           not blank_node_id?(graph_name) do
        dataset
      else
        # 1.3)
        rdf_graph =
          graph
          |> Enum.sort_by(fn {subject, _} -> subject end)
          |> Enum.reduce(Graph.new(), fn {subject, node}, rdf_graph ->
            # 1.3.1)
            if not well_formed_iri?(subject) and not blank_node_id?(subject) do
              rdf_graph
            else
              # 1.3.2)
              node
              |> Enum.sort_by(fn {property, _} -> property end)
              |> Enum.reduce(rdf_graph, fn {property, values}, rdf_graph ->
                cond do
                  # 1.3.2.1)
                  property == "@type" ->
                    if subject = node_to_rdf(subject) do
                      objects = values |> Enum.map(&node_to_rdf/1) |> Enum.reject(&is_nil/1)

                      Graph.add(rdf_graph, {subject, NS.RDF.type(), objects})
                    else
                      rdf_graph
                    end

                  # 1.3.2.2)
                  JSON.LD.keyword?(property) ->
                    rdf_graph

                  # 1.3.2.3)
                  not options.produce_generalized_rdf and blank_node_id?(property) ->
                    rdf_graph

                  # 1.3.2.4)
                  not well_formed_iri?(property) ->
                    rdf_graph

                  # 1.3.2.5)
                  true ->
                    Enum.reduce(values, rdf_graph, fn item, rdf_graph ->
                      case object_to_rdf(item, node_id_map, options) do
                        {_list_triples, nil} ->
                          rdf_graph

                        {list_triples, object} ->
                          rdf_graph
                          |> Graph.add({node_to_rdf(subject), node_to_rdf(property), object})
                          |> Graph.add(list_triples)
                      end
                    end)
                end
              end)
            end
          end)

        if Enum.empty?(rdf_graph) do
          dataset
        else
          graph_name = if graph_name == "@default", do: nil, else: graph_name
          Dataset.add(dataset, rdf_graph, graph: graph_name)
        end
      end
    end)
  end

  defp well_formed_iri?(iri) do
    valid_uri?(iri)
  end

  @spec parse_json(String.t(), [Jason.decode_opt()]) ::
          {:ok, map} | {:error, Jason.DecodeError.t()}
  def parse_json(content, _opts \\ []) do
    Jason.decode(content)
  end

  @spec parse_json!(String.t(), [Jason.decode_opt()]) :: map
  def parse_json!(content, _opts \\ []) do
    Jason.decode!(content)
  end

  @spec node_to_rdf(String.t()) :: IRI.t() | BlankNode.t() | nil
  def node_to_rdf(node) do
    cond do
      blank_node_id?(node) -> node |> String.trim_leading("_:") |> RDF.bnode()
      well_formed_iri?(node) -> RDF.uri(node)
      true -> nil
    end
  end

  # Object to RDF Conversion - https://www.w3.org/TR/json-ld11-api/#object-to-rdf-conversion
  defp object_to_rdf(item, node_id_map, options)
  # 1) and 2)
  defp object_to_rdf(%{"@id" => id}, _node_id_map, _options) do
    {[], node_to_rdf(id)}
  end

  # 3)
  defp object_to_rdf(%{"@list" => list}, node_id_map, options) do
    list_to_rdf(list, node_id_map, options)
  end

  # 4)
  defp object_to_rdf(%{"@value" => value} = item, _node_id_map, options) do
    # 5)
    datatype = item["@type"]

    {value, datatype} =
      cond do
        # 6)
        not is_nil(datatype) and relative_iri?(datatype) and datatype != "@json" ->
          {nil, datatype}

        # 7)
        Map.has_key?(item, "@language") and not relative_iri?(item["@language"]) ->
          {nil, datatype}

        # 8)
        datatype == "@json" ->
          value =
            value
            |> RDF.JSON.new(as_value: true)
            |> RDF.JSON.canonical()
            |> RDF.JSON.lexical()

          {value, RDF.iri(RDF.JSON)}

        # 9)
        is_boolean(value) ->
          value =
            value
            |> XSD.Boolean.new()
            |> XSD.Boolean.canonical()
            |> XSD.Boolean.lexical()

          datatype = if is_nil(datatype), do: NS.XSD.boolean(), else: datatype
          {value, datatype}

        # 10)
        is_number(value) and
            (datatype == to_string(NS.XSD.double()) or value != trunc(value) or
               value >= 1.0e21) ->
          value =
            value
            |> XSD.Double.new()
            |> XSD.Double.canonical()
            |> XSD.Double.lexical()

          datatype = if is_nil(datatype), do: NS.XSD.double(), else: datatype
          {value, datatype}

        # 11)
        is_number(value) ->
          value =
            if(is_float(value), do: trunc(value), else: value)
            |> XSD.Integer.new()
            |> XSD.Integer.canonical()
            |> XSD.Integer.lexical()

          datatype = if is_nil(datatype), do: NS.XSD.integer(), else: datatype
          {value, datatype}

        # 12)
        is_nil(datatype) ->
          datatype =
            if Map.has_key?(item, "@language"), do: RDF.langString(), else: NS.XSD.string()

          {value, datatype}

        true ->
          {value, datatype}
      end

    cond do
      is_nil(value) ->
        {[], nil}

      # 13)
      Map.has_key?(item, "@direction") and not is_nil(options.rdf_direction) ->
        # 13.1)
        language = String.downcase(item["@language"] || "")
        direction = item["@direction"]

        case options.rdf_direction do
          # 13.2)
          "i18n-datatype" ->
            {[],
             Literal.new(value, datatype: "https://www.w3.org/ns/i18n##{language}_#{direction}")}

          # 13.3)
          "compound-literal" ->
            literal = RDF.bnode()

            list_triples =
              [
                {literal, RDF.value(), value},
                {literal, @rdf_direction, direction}
              ]

            list_triples =
              if Map.has_key?(item, "@language") do
                [{literal, @rdf_language, language} | list_triples]
              else
                list_triples
              end

            {list_triples, literal}
        end

      # 14)
      language = item["@language"] ->
        {
          [],
          if(valid_language?(language),
            do: Literal.new(value, language: language, canonicalize: true)
          )
        }

      true ->
        {[], Literal.new(value, datatype: datatype, canonicalize: true)}
    end
  end

  @spec list_to_rdf([map], pid, Options.t()) :: {[Statement.t()], IRI.t() | BlankNode.t()}
  defp list_to_rdf(list, node_id_map, options) do
    {list_triples, first, last} =
      Enum.reduce(list, {[], nil, nil}, fn item, {list_triples, first, last} ->
        case object_to_rdf(item, node_id_map, options) do
          {more_list_triples, object} ->
            bnode = RDF.bnode(generate_blank_node_id(node_id_map))
            list_triples = more_list_triples ++ list_triples
            object_triples = if object, do: [{bnode, NS.RDF.first(), object}], else: []

            if last do
              {list_triples ++ [{last, NS.RDF.rest(), bnode} | object_triples], first, bnode}
            else
              {object_triples ++ list_triples, bnode, bnode}
            end
        end
      end)

    if last do
      {list_triples ++ [{last, NS.RDF.rest(), NS.RDF.nil()}], first}
    else
      {[], NS.RDF.nil()}
    end
  end
end
