// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef MEDIA_MIDI_MIDI_MANAGER_ALSA_H_
#define MEDIA_MIDI_MIDI_MANAGER_ALSA_H_

#include <alsa/asoundlib.h>
#include <stdint.h>

#include <map>
#include <memory>
#include <utility>
#include <vector>

#include "base/containers/hash_tables.h"
#include "base/gtest_prod_util.h"
#include "base/macros.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread.h"
#include "base/values.h"
#include "device/udev_linux/scoped_udev.h"
#include "media/midi/midi_export.h"
#include "media/midi/midi_manager.h"

namespace base {
class ThreadChecker;
}

namespace media {
namespace midi {

class MIDI_EXPORT MidiManagerAlsa final : public MidiManager {
 public:
  MidiManagerAlsa();
  ~MidiManagerAlsa() override;

  // MidiManager implementation.
  void StartInitialization() override;
  void Finalize() override;
  void DispatchSendMidiData(MidiManagerClient* client,
                            uint32_t port_index,
                            const std::vector<uint8_t>& data,
                            double timestamp) override;

 private:
  friend class MidiManagerAlsaTest;
  FRIEND_TEST_ALL_PREFIXES(MidiManagerAlsaTest, ExtractManufacturer);
  FRIEND_TEST_ALL_PREFIXES(MidiManagerAlsaTest, ToMidiPortState);

  class AlsaCard;
  using AlsaCardMap = std::map<int, std::unique_ptr<AlsaCard>>;

  class MidiPort {
   public:
    enum class Type { kInput, kOutput };

    // The Id class is used to keep the multiple strings separate
    // but compare them all together for equality purposes.
    // The individual strings that make up the Id can theoretically contain
    // arbitrary characters, so unfortunately there is no simple way to
    // concatenate them into a single string.
    class Id final {
     public:
      Id();
      Id(const std::string& bus,
         const std::string& vendor_id,
         const std::string& model_id,
         const std::string& usb_interface_num,
         const std::string& serial);
      Id(const Id&);
      ~Id();
      bool operator==(const Id&) const;
      bool empty() const;

      std::string bus() const { return bus_; }
      std::string vendor_id() const { return vendor_id_; }
      std::string model_id() const { return model_id_; }
      std::string usb_interface_num() const { return usb_interface_num_; }
      std::string serial() const { return serial_; }

     private:
      std::string bus_;
      std::string vendor_id_;
      std::string model_id_;
      std::string usb_interface_num_;
      std::string serial_;
    };

    MidiPort(const std::string& path,
             const Id& id,
             int client_id,
             int port_id,
             int midi_device,
             const std::string& client_name,
             const std::string& port_name,
             const std::string& manufacturer,
             const std::string& version,
             Type type);
    ~MidiPort();

    // Gets a Value representation of this object, suitable for serialization.
    std::unique_ptr<base::Value> Value() const;

    // Gets a string version of Value in JSON format.
    std::string JSONValue() const;

    // Gets an opaque identifier for this object, suitable for using as the id
    // field in MidiPort.id on the web. Note that this string does not store
    // the full state.
    std::string OpaqueKey() const;

    // Checks for equality for connected ports.
    bool MatchConnected(const MidiPort& query) const;
    // Checks for equality for kernel cards with id, pass 1.
    bool MatchCardPass1(const MidiPort& query) const;
    // Checks for equality for kernel cards with id, pass 2.
    bool MatchCardPass2(const MidiPort& query) const;
    // Checks for equality for non-card clients, pass 1.
    bool MatchNoCardPass1(const MidiPort& query) const;
    // Checks for equality for non-card clients, pass 2.
    bool MatchNoCardPass2(const MidiPort& query) const;

    // accessors
    std::string path() const { return path_; }
    Id id() const { return id_; }
    std::string client_name() const { return client_name_; }
    std::string port_name() const { return port_name_; }
    std::string manufacturer() const { return manufacturer_; }
    std::string version() const { return version_; }
    int client_id() const { return client_id_; }
    int port_id() const { return port_id_; }
    int midi_device() const { return midi_device_; }
    Type type() const { return type_; }
    uint32_t web_port_index() const { return web_port_index_; }
    bool connected() const { return connected_; }

    // mutators
    void set_web_port_index(uint32_t web_port_index) {
      web_port_index_ = web_port_index;
    }
    void set_connected(bool connected) { connected_ = connected; }
    void Update(const std::string& path,
                int client_id,
                int port_id,
                const std::string& client_name,
                const std::string& port_name,
                const std::string& manufacturer,
                const std::string& version) {
      path_ = path;
      client_id_ = client_id;
      port_id_ = port_id;
      client_name_ = client_name;
      port_name_ = port_name;
      manufacturer_ = manufacturer;
      version_ = version;
    }

   private:
    // Immutable properties.
    const Id id_;
    const int midi_device_;

    const Type type_;

    // Mutable properties. These will get updated as ports move around or
    // drivers change.
    std::string path_;
    int client_id_;
    int port_id_;
    std::string client_name_;
    std::string port_name_;
    std::string manufacturer_;
    std::string version_;

    // Index for MidiManager.
    uint32_t web_port_index_ = 0;

    // Port is present in the ALSA system.
    bool connected_ = true;

    DISALLOW_COPY_AND_ASSIGN(MidiPort);
  };

  class MidiPortStateBase {
   public:
    typedef std::vector<std::unique_ptr<MidiPort>>::iterator iterator;

    virtual ~MidiPortStateBase();

    // Given a port, finds a port in the internal store.
    iterator Find(const MidiPort& port);

    // Given a port, finds a connected port, using exact matching.
    iterator FindConnected(const MidiPort& port);

    // Given a port, finds a disconnected port, using heuristic matching.
    iterator FindDisconnected(const MidiPort& port);

    iterator begin() { return ports_.begin(); }
    iterator end() { return ports_.end(); }

   protected:
    MidiPortStateBase();
    iterator erase(iterator position) { return ports_.erase(position); }
    void push_back(std::unique_ptr<MidiPort> port) {
      ports_.push_back(std::move(port));
    }

   private:
    std::vector<std::unique_ptr<MidiPort>> ports_;

    DISALLOW_COPY_AND_ASSIGN(MidiPortStateBase);
  };

  class TemporaryMidiPortState final : public MidiPortStateBase {
   public:
    iterator erase(iterator position) {
      return MidiPortStateBase::erase(position);
    };
    void push_back(std::unique_ptr<MidiPort> port) {
      MidiPortStateBase::push_back(std::move(port));
    }
  };

  class MidiPortState final : public MidiPortStateBase {
   public:
    MidiPortState();

    // Inserts a port at the end. Returns web_port_index.
    uint32_t push_back(std::unique_ptr<MidiPort> port);

   private:
    uint32_t num_input_ports_ = 0;
    uint32_t num_output_ports_ = 0;
  };

  class AlsaSeqState {
   public:
    enum class PortDirection { kInput, kOutput, kDuplex };

    AlsaSeqState();
    ~AlsaSeqState();

    void ClientStart(int client_id,
                     const std::string& client_name,
                     snd_seq_client_type_t type);
    bool ClientStarted(int client_id);
    void ClientExit(int client_id);
    void PortStart(int client_id,
                   int port_id,
                   const std::string& port_name,
                   PortDirection direction,
                   bool midi);
    void PortExit(int client_id, int port_id);
    snd_seq_client_type_t ClientType(int client_id) const;
    std::unique_ptr<TemporaryMidiPortState> ToMidiPortState(
        const AlsaCardMap& alsa_cards);

    int card_client_count() { return card_client_count_; }

   private:
    class Port {
     public:
      Port(const std::string& name, PortDirection direction, bool midi);
      ~Port();

      std::string name() const { return name_; }
      PortDirection direction() const { return direction_; }
      // True if this port is a MIDI port, instead of another kind of ALSA port.
      bool midi() const { return midi_; }

     private:
      const std::string name_;
      const PortDirection direction_;
      const bool midi_;

      DISALLOW_COPY_AND_ASSIGN(Port);
    };

    class Client {
     public:
      using PortMap = std::map<int, std::unique_ptr<Port>>;

      Client(const std::string& name, snd_seq_client_type_t type);
      ~Client();

      std::string name() const { return name_; }
      snd_seq_client_type_t type() const { return type_; }
      void AddPort(int addr, std::unique_ptr<Port> port);
      void RemovePort(int addr);
      PortMap::const_iterator begin() const;
      PortMap::const_iterator end() const;

     private:
      const std::string name_;
      const snd_seq_client_type_t type_;
      PortMap ports_;

      DISALLOW_COPY_AND_ASSIGN(Client);
    };

    std::map<int, std::unique_ptr<Client>> clients_;

    // This is the current number of clients we know about that have
    // cards. When this number matches alsa_card_midi_count_, we know
    // we are in sync between ALSA and udev. Until then, we cannot generate
    // MIDIConnectionEvents to web clients.
    int card_client_count_ = 0;

    DISALLOW_COPY_AND_ASSIGN(AlsaSeqState);
  };

  class AlsaCard {
   public:
    AlsaCard(udev_device* dev,
             const std::string& name,
             const std::string& longname,
             const std::string& driver,
             int midi_device_count);
    ~AlsaCard();
    std::string name() const { return name_; }
    std::string longname() const { return longname_; }
    std::string driver() const { return driver_; }
    std::string path() const { return path_; }
    std::string bus() const { return bus_; }
    std::string vendor_id() const { return vendor_id_; }
    std::string model_id() const { return model_id_; }
    std::string usb_interface_num() const { return usb_interface_num_; }
    std::string serial() const { return serial_; }
    int midi_device_count() const { return midi_device_count_; }
    std::string manufacturer() const { return manufacturer_; }

   private:
    FRIEND_TEST_ALL_PREFIXES(MidiManagerAlsaTest, ExtractManufacturer);

    // Extracts the manufacturer using heuristics and a variety of sources.
    static std::string ExtractManufacturerString(
        const std::string& udev_id_vendor,
        const std::string& udev_id_vendor_id,
        const std::string& udev_id_vendor_from_database,
        const std::string& name,
        const std::string& longname);

    const std::string name_;
    const std::string longname_;
    const std::string driver_;
    const std::string path_;
    const std::string bus_;
    const std::string vendor_id_;
    const std::string model_id_;
    const std::string usb_interface_num_;
    const std::string serial_;
    const int midi_device_count_;
    const std::string manufacturer_;

    DISALLOW_COPY_AND_ASSIGN(AlsaCard);
  };

  struct SndSeqDeleter {
    void operator()(snd_seq_t* seq) const { snd_seq_close(seq); }
  };

  struct SndMidiEventDeleter {
    void operator()(snd_midi_event_t* coder) const {
      snd_midi_event_free(coder);
    };
  };

  using SourceMap = base::hash_map<int, uint32_t>;
  using OutPortMap = base::hash_map<uint32_t, int>;
  using ScopedSndSeqPtr = std::unique_ptr<snd_seq_t, SndSeqDeleter>;
  using ScopedSndMidiEventPtr =
      std::unique_ptr<snd_midi_event_t, SndMidiEventDeleter>;

  // An internal callback that runs on MidiSendThread.
  void SendMidiData(uint32_t port_index, const std::vector<uint8_t>& data);

  void ScheduleEventLoop();
  void EventLoop();
  void ProcessSingleEvent(snd_seq_event_t* event, double timestamp);
  void ProcessClientStartEvent(int client_id);
  void ProcessPortStartEvent(const snd_seq_addr_t& addr);
  void ProcessClientExitEvent(const snd_seq_addr_t& addr);
  void ProcessPortExitEvent(const snd_seq_addr_t& addr);
  void ProcessUdevEvent(udev_device* dev);
  void AddCard(udev_device* dev);
  void RemoveCard(int number);

  // Updates port_state_ and Web MIDI state from alsa_seq_state_.
  void UpdatePortStateAndGenerateEvents();

  // Enumerates ports. Call once after subscribing to the announce port.
  void EnumerateAlsaPorts();
  // Enumerates udev cards. Call once after initializing the udev monitor.
  bool EnumerateUdevCards();
  // Returns true if successful.
  bool CreateAlsaOutputPort(uint32_t port_index, int client_id, int port_id);
  void DeleteAlsaOutputPort(uint32_t port_index);
  // Returns true if successful.
  bool Subscribe(uint32_t port_index, int client_id, int port_id);

  // Members initialized in the constructor are below.
  // Our copies of the internal state of the ports of seq and udev.
  AlsaSeqState alsa_seq_state_;
  MidiPortState port_state_;

  // One input port, many output ports.
  base::Lock out_ports_lock_;  // guards out_ports_
  OutPortMap out_ports_;       // guarded by out_ports_lock_

  // Mapping from ALSA client:port to our index.
  SourceMap source_map_;

  // Mapping from card to devices.
  AlsaCardMap alsa_cards_;

  // This is the current count of midi devices across all cards we know
  // about. When this number matches card_client_count_ in AlsaSeqState,
  // we are safe to generate MIDIConnectionEvents. Otherwise we need to
  // wait for our information from ALSA and udev to get back in sync.
  int alsa_card_midi_count_ = 0;

  base::Lock shutdown_lock_;            // guards event_thread_shutdown_
  bool event_thread_shutdown_ = false;  // guarded by shutdown_lock_

  // This lock is needed to ensure that members destroyed in Finalize
  // will be visibly destroyed before the destructor is run in the
  // other thread. Otherwise, the same objects may have their destructors
  // run multiple times in different threads.
  base::Lock lazy_init_member_lock_;  // guards members below

  // Members initialized in StartInitialization() are below.
  // Make sure to destroy these in Finalize()!
  std::unique_ptr<base::ThreadChecker> initialization_thread_checker_;

  // ALSA seq handles and ids.
  ScopedSndSeqPtr in_client_;
  int in_client_id_;
  ScopedSndSeqPtr out_client_;
  int out_client_id_;
  int in_port_id_;

  // ALSA event -> MIDI coder.
  ScopedSndMidiEventPtr decoder_;

  // udev, for querying hardware devices.
  device::ScopedUdevPtr udev_;
  device::ScopedUdevMonitorPtr udev_monitor_;

  // Threads for sending and receiving. These are initialized in the
  // constructor, but are started at the end of StartInitialization.
  base::Thread event_thread_;
  base::Thread send_thread_;

  DISALLOW_COPY_AND_ASSIGN(MidiManagerAlsa);
};

}  // namespace midi
}  // namespace media

#endif  // MEDIA_MIDI_MIDI_MANAGER_ALSA_H_
