/**
 * This code is part of Qiskit.
 *
 * (C) Copyright IBM 2018, 2020.
 *
 * This code is licensed under the Apache License, Version 2.0. You may
 * obtain a copy of this license in the LICENSE.txt file in the root directory
 * of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Any modifications or derivative works of this code must retain this
 * copyright notice, and modified files need to carry a notice indicating
 * that they have been altered from the originals.
 */

#ifndef _aer_framework_opset_hpp_
#define _aer_framework_opset_hpp_

#include "framework/operations.hpp"
#include <algorithm>

namespace AER {
namespace Operations {

//=========================================================================
// OpSet Class
//=========================================================================

// This class is used to store type information about a set of operations.
class OpSet {
private:
  // Hash function so that we can use an enum class as a std::unordered_set
  // key on older C++11 compilers like GCC 5.
  struct EnumClassHash {
    template <typename T>
    size_t operator()(T t) const {
      return static_cast<size_t>(t);
    }
  };

public:
  // Alias for set of OpTypes
  using optypeset_t = std::unordered_set<Operations::OpType, EnumClassHash>;

  // Public data members
  optypeset_t optypes; // A set of op types
  stringset_t gates;   // A set of names for OpType::gates

  OpSet() = default;

  OpSet(const optypeset_t &_optypes, const stringset_t &_gates)
      : optypes(_optypes), gates(_gates) {}

  OpSet(optypeset_t &&_optypes, stringset_t &&_gates)
      : optypes(std::move(_optypes)), gates(std::move(_gates)) {}

  OpSet(const std::vector<Op> &ops) {
    for (const auto &op : ops) {
      insert(op);
    }
  }

  //-----------------------------------------------------------------------
  // Insert operations to the OpSet
  //-----------------------------------------------------------------------

  // Add another opset to the current one
  void insert(const OpSet &_opset);

  // Add additional op to the opset
  void insert(const Op &_op);

  //-----------------------------------------------------------------------
  // Check if operations are in the OpSet
  //-----------------------------------------------------------------------

  // Return true if another OpSet is contained in the current OpSet
  bool contains(const OpSet &_opset) const;

  // Return true if an all operations are contained in the current OpSet
  bool contains(const std::vector<Op> &_ops) const;

  // Return true if an operation is contained in the current OpSet
  bool contains(const Op &_op) const;

  // Return true if all operation types are contained in the current OpSet
  bool contains(const optypeset_t &_optypes) const;

  // Return true if an operation type is contained in the current OpSet
  bool contains(const OpType &_optype) const;

  // Return true if all gates are contained in the current OpSet
  bool contains_gates(const stringset_t &_gates) const;

  // Return true if gate is contained in the current OpSet
  bool contains_gates(const std::string &_gate) const;

  //-----------------------------------------------------------------------
  // Return set difference with another OpSet
  //-----------------------------------------------------------------------

  // Return an OpSet of all ops in another opset not contained in the OpSet
  OpSet difference(const OpSet &_opset) const;

  // Return an Opset of all ops in a vector not contained in the OpSet
  OpSet difference(const std::vector<Op> &_ops) const;

  // Return a set of all optypes in set not contained in the OpSet
  optypeset_t difference(const optypeset_t &_optypes) const;

  // Return a set of all gates in a set not contained in the OpSet
  stringset_t difference_gates(const stringset_t &_gates) const;

  // Return the difference between two unordered sets
  template <typename T1, typename T2>
  static std::unordered_set<T1, T2>
  unorderedset_difference(const std::unordered_set<T1, T2> &first,
                          const std::unordered_set<T1, T2> &second);
};

//------------------------------------------------------------------------------
// OpSet class methods
//------------------------------------------------------------------------------

void OpSet::insert(const Op &op) {
  optypes.insert(op.type);
  if (op.type == OpType::gate)
    gates.insert(op.name);
}

void OpSet::insert(const OpSet &opset) {
  optypes.insert(opset.optypes.begin(), opset.optypes.end());
  gates.insert(opset.gates.begin(), opset.gates.end());
}

bool OpSet::contains(const OpSet &_opset) const {
  return (contains(_opset.optypes) && contains_gates(_opset.gates));
}

bool OpSet::contains(const Op &_op) const {
  if (contains(_op.type)) {
    if (_op.type == OpType::gate)
      return contains_gates(_op.name);
    return true;
  }
  return false;
}

bool OpSet::contains(const std::vector<Op> &_ops) const {
  for (const auto &op : _ops) {
    if (!contains(op))
      return false;
  }
  return true;
}

bool OpSet::contains(const OpType &_optype) const {
  return optypes.count(_optype) == 1;
}

bool OpSet::contains(const optypeset_t &_optypes) const {
  for (const auto &optype : _optypes) {
    if (!contains(optype))
      return false;
  }
  return true;
}

bool OpSet::contains_gates(const std::string &_gate) const {
  return gates.count(_gate) == 1;
}

bool OpSet::contains_gates(const stringset_t &_gates) const {
  for (const auto &gate : _gates) {
    if (!contains_gates(gate))
      return false;
  }
  return true;
}

//-----------------------------------------------------------------------
// Return set difference with another OpSet
//-----------------------------------------------------------------------

// Return an OpSet of all ops in another opset not contained in the OpSet
OpSet OpSet::difference(const OpSet &_opset) const {
  OpSet ret;
  ret.optypes = difference(_opset.optypes);
  ret.gates = difference_gates(_opset.gates);
  return ret;
}

// Return a set of all optypes in set not contained in the OpSet
OpSet::optypeset_t OpSet::difference(const optypeset_t &_optypes) const {
  return unorderedset_difference(optypes, _optypes);
}

// Return a set of all gates in a set not contained in the OpSet
stringset_t OpSet::difference_gates(const stringset_t &_gates) const {
  return unorderedset_difference(gates, _gates);
}

template <typename T1, typename T2>
std::unordered_set<T1, T2>
OpSet::unorderedset_difference(const std::unordered_set<T1, T2> &first,
                               const std::unordered_set<T1, T2> &second) {
  std::unordered_set<T1, T2> ret;
  for (const auto &item : second) {
    if (first.count(item) == 0) {
      ret.insert(item);
    }
  }
  return ret;
}

//------------------------------------------------------------------------------
} // end namespace Operations
} // end namespace AER
//------------------------------------------------------------------------------

//-------------------------------------------------------------------------
// Ostream overload for opset
//-------------------------------------------------------------------------

inline std::ostream &operator<<(std::ostream &out,
                                const AER::Operations::OpSet &opset) {
  bool first = true;
  out << "{";
  if (!opset.optypes.empty()) {
    out << "\"instructions\": " << opset.optypes;
    first = false;
  }
  if (!opset.gates.empty()) {
    if (!first)
      out << ", ";
    out << "\"gates\": " << opset.gates;
  }
  out << "}";
  return out;
}

//------------------------------------------------------------------------------
#endif
