#pragma once

#include <sstream>
#include "../enum/gm.h"
#include "midi.pb.h"


// use this code to validate protobuf

int check_events(midi::Piece *x) {
  int num_events = x->events_size();
  for (const auto track : x->tracks()) {
    for (const auto bar : track.bars()) {
      int bar_ticks = bar.beat_length() * x->resolution();
      for (const auto event_index : bar.events()) {
        // make sure that event exists in x->events
        if ((event_index < 0) || (event_index)) {
          return false;
        }
        // make sure that there s no overflow in terms of time
        bool is_onset = x->events(event_index).velocity() > 0;
        int tick = x->events(event_index).time();
        if ((tick > bar_ticks) || (is_onset && (tick >= bar_ticks))) {
          return false;
        }
      }
    }
  }
  return true;
}

bool check_track_lengths(midi::Piece *x) {
  int num_tracks = x->tracks_size();
  if (num_tracks > 0) {
    int num_bars = x->tracks(0).bars_size();
    for (int track_num=1; track_num<num_tracks; track_num++) {
      if (num_bars != x->tracks(track_num).bars_size()) {
        return false;
      }
    }
  }
  return true;
}


void validate(midi::Piece *x) {

  // check that piece has resolution
  // is there other metadata to check
  assert(x->resolution() > 0);

  // check that there are the same number of bars in each track
  assert(check_track_lengths(x));

  // check events are valid
  // event_index should reference valid event
  // event times should be within each bar
  assert(check_events(x));

}

// update has notes information
void prepare_piece(midi::Piece *x) {
  for (int track_num=0; track_num<x->tracks_size(); track_num++) {
    midi::Track *track = x->mutable_tracks(track_num);
    for (int bar_num=0; bar_num<track->bars_size(); bar_num++) {
      midi::Bar *bar = track->mutable_bars(bar_num);
      bar->set_is_drum(track->is_drum());
    }
  }
}

template <typename T>
void print_set(set<T> &values) {
  cout << "{";
  for (const auto v : values) {
    cout << v << ",";
  }
  cout << "}";
}

template <typename T>
void check_range(T value, T minv, T maxv, const char *field) {
  if ((value < minv) || (value >= maxv)) {
    ostringstream buffer;
    buffer << field << " not on range [" << minv << "," << maxv << ").";
    throw std::invalid_argument(buffer.str());
  }
} 

template <typename T> 
void check_all_same(set<T> &values, const char *field) {
  if (values.size() != 1) {
    ostringstream buffer;
    buffer << field << " values must all be the same. {";
    for (const auto val : values) {
      buffer << val << ",";
    }
    buffer << "}";
    throw std::invalid_argument(buffer.str());
  }
}

template <typename T>
void check_all_different(set<T> &values, int n, const char *field) {
  if (values.size() != n) {
    ostringstream buffer;
    buffer << field << " values must all be different.";
    throw std::invalid_argument(buffer.str());
  }
}

template <typename T> 
void check_in_domain(T value, set<T> domain, const char *field) {
  if (domain.find( value ) == domain.end()) {
    ostringstream buffer;
    buffer << field << " not in domain.";
    throw std::invalid_argument(buffer.str());
  }
}

int count_selected_bars(const midi::StatusTrack &track) {
  int count = 0;
  for (const auto selected : track.selected_bars()) {
    count += (int)selected;
  }
  return count;
}

enum STATUS_TRACK_TYPE {
  CONDITION,
  RESAMPLE,
  INFILL
};

STATUS_TRACK_TYPE infer_track_type(const midi::StatusTrack &track) {
  int num_bars = track.selected_bars_size();
  int bar_count = count_selected_bars(track);
  if (bar_count == 0) {
    return CONDITION;
  }
  else if (bar_count != num_bars) {
    return INFILL;
  }
  return RESAMPLE;
}

void validate_status(midi::Status *status, midi::Piece *piece) {

  if ((!status) || (!piece)) {
    throw std::invalid_argument("NULL INPUT");
  }

  if (status->tracks_size() == 0) {
    throw std::invalid_argument("STATUS IS EMPTY");
  }

  int track_num = 0;
  for (const auto track : status->tracks()) {

    
    if (!track.has_track_id()) {
      throw std::invalid_argument("NO TRACK ID");
    }
    if (!track.has_track_type()) {
      throw std::invalid_argument("NO TRACK TYPE");
    }
    if (!track.has_instrument()) {
      throw std::invalid_argument("NO INSTRUMENT");
    }
    if (!track.has_density()) {
      throw std::invalid_argument("NO DENSITY");
    }
    if (track.selected_bars_size() == 0) {
      throw std::invalid_argument("NO SELECTED BARS");
    }
    
    // if track is conditioning it must be within range
    STATUS_TRACK_TYPE tt = infer_track_type(track);
    if ((tt == CONDITION) || (tt == INFILL)) {
      check_range(track.track_id(), 0, piece->tracks_size(), "track_id");
      // check that the instruments match
      int piece_inst = piece->tracks(track.track_id()).instrument();
      int status_inst = GM[track.instrument()][0] % 128;
      if (piece_inst != status_inst) {
        ostringstream buffer;
        buffer << "instruments differ on track " << track.track_id() << "(" << piece_inst << " != " << status_inst << ")";
        throw std::invalid_argument(buffer.str());
      }
    }
    check_in_domain(track.instrument(), GM_KEYS, "instrument");
    check_range(track.density(), -1, 10, "density");
    check_in_domain(
      track.selected_bars_size(), {4,8,16}, "sample_bars (length)");
    
    track_num++;
  }

  // check track lengths and track_ids
  set<int> track_lengths;
  set<int> track_ids;
  for (const auto track : status->tracks()) {
    track_lengths.insert( track.selected_bars_size() );
    track_ids.insert( track.track_id() );
  }
  check_all_same(track_lengths, "sample_bars (length)");
  check_all_different(track_ids, status->tracks_size(), "track_id");

  // check that all tracks are referenced in status
  // we don't need to have this gauranteed .
  /*
  for (int track_num=0; track_num<piece->tracks_size(); track_num++) {
    if (track_ids.find(track_num) == track_ids.end()) {
      ostringstream buffer;
      buffer << "Track " << track_num << " is not found in status!";
      throw std::invalid_argument(buffer.str());
    }
  }
  */

  // check that instruments match between piece and status
}