-- WirePlumber
--
-- Copyright © 2022 Collabora Ltd.
--
-- SPDX-License-Identifier: MIT

-- looks for changes in user-preferences and devices added/removed and schedules
-- rescan and pushes "select-default-node" event for each of the media_classes

log = Log.open_topic ("s-default-nodes")

-- looks for changes in user-preferences and devices added/removed and schedules
-- rescan
SimpleEventHook {
  name = "default-nodes/rescan-trigger",
  interests = {
    EventInterest {
      Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "media.class", "#", "Audio/*" },
    },
    EventInterest {
      Constraint { "event.type", "c", "session-item-added", "session-item-removed" },
      Constraint { "event.session-item.interface", "=", "linkable" },
      Constraint { "media.class", "#", "Video/*" },
    },
    EventInterest {
      Constraint { "event.type", "=", "metadata-changed" },
      Constraint { "metadata.name", "=", "default" },
      Constraint { "event.subject.key", "c", "default.configured.audio.sink",
          "default.configured.audio.source", "default.configured.video.source"
      },
    },
    EventInterest {
      Constraint { "event.type", "=", "device-params-changed"},
      Constraint { "event.subject.param-id", "c", "Route", "EnumRoute"},
    },
  },
  execute = function (event)
    local source = event:get_source ()
    source:call ("schedule-rescan", "default-nodes")
  end
}:register ()

-- pushes "select-default-node" event for each of the media_classes
SimpleEventHook {
  name = "default-nodes/rescan",
  interests = {
    EventInterest {
      Constraint { "event.type", "=", "rescan-for-default-nodes" },
    },
  },
  execute = function (event)
    local source = event:get_source ()
    local si_om = source:call ("get-object-manager", "session-item")
    local devices_om = source:call ("get-object-manager", "device")

    log:trace ("re-evaluating default nodes")

    -- Audio Sink
    pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.sink", "in", {
      "Audio/Sink", "Audio/Duplex"
    })

    -- Audio Source
    pushSelectDefaultNodeEvent (source, si_om, devices_om, "audio.source", "out", {
      "Audio/Source", "Audio/Source/Virtual", "Audio/Duplex", "Audio/Sink"
    })

    -- Video Source
    pushSelectDefaultNodeEvent (source, si_om, devices_om, "video.source", "out", {
      "Video/Source", "Video/Source/Virtual"
    })
  end
}:register ()

function pushSelectDefaultNodeEvent (source, si_om, devices_om, def_node_type,
                                     port_direction, media_classes)
  local nodes =
      collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
  local event = source:call ("create-event", "select-default-node", nil, {
      ["default-node.type"] = def_node_type,
  })
  event:set_data ("available-nodes", Json.Array (nodes))
  EventDispatcher.push_event (event)
end

-- Return an array table where each element is another table containing all the
-- node properties of all the nodes that can be selected for a given media class
-- set and direction
function collectAvailableNodes (si_om, devices_om, port_direction, media_classes)
  local collected = {}

  for linkable in si_om:iterate {
    type = "SiLinkable",
    Constraint { "media.class", "c", table.unpack (media_classes) },
  } do
    local linkable_props = linkable.properties
    local node = linkable:get_associated_proxy ("node")

    -- check that the node has ports in the requested direction
    if not node:lookup_port {
      Constraint { "port.direction", "=", port_direction }
    } then
      goto next_linkable
    end

    -- check that the node has available routes,
    -- if it is associated to a real device
    if not nodeHasAvailableRoutes (node, devices_om) then
      goto next_linkable
    end

    table.insert (collected, Json.Object (node.properties))

    ::next_linkable::
  end

  return collected
end

-- If the node has an associated device, verify that it has an available
-- route. Some UCM profiles expose all paths (headphones, HDMI, etc) as nodes,
-- even though they may not be connected... See #145
function nodeHasAvailableRoutes (node, devices_om)
  local properties = node.properties
  local device_id = properties ["device.id"]
  local cpd = properties ["card.profile.device"]

  if not device_id or not cpd then
    return true
  end

  -- Get the device
  local device = devices_om:lookup {
    Constraint { "bound-id", "=", device_id, type = "gobject" }
  }
  if not device then
    return true
  end

  --  Check if the current device route supports the node card device profile
  for r in device:iterate_params ("Route") do
    local route = r:parse ()
    local route_props = route.properties
    if route_props.device == tonumber (cpd) then
      if route_props.available == "no" then
        return false
      else
        return true
      end
    end
  end

  -- Check if available routes support the node card device profile
  local found = 0
  for r in device:iterate_params ("EnumRoute") do
    local route = r:parse ()
    local route_props = route.properties
    if type (route_props.devices) == "table" then
      for _, i in ipairs (route_props.devices) do
        if i == tonumber (cpd) then
          found = found + 1
          if route_props.available ~= "no" then
            return true
          end
        end
      end
    end
  end

  -- The node is part of a profile without routes so we assume it
  -- is available. This can happen for Pro Audio profiles
  if found == 0 then
    return true
  end

  return false
end
