#ifndef PARM_TABLE_H
#define PARM_TABLE_H

#include <map>
#include <set>
#include <stdexcept>

#include <casacore/ms/MeasurementSets/MSTable.h>

#include <casacore/tables/Tables/ArrayColumn.h>
#include <casacore/tables/Tables/ScalarColumn.h>

#include "../util/logger.h"

#include "../structures/image2d.h"
#include "../structures/timefrequencydata.h"

class ParmTable {
 public:
  struct GainNameEntry {
    int index;
    int x, y;
    enum Component { Real, Imaginary } component;
    std::string antenna;

    GainNameEntry() : index(0), x(0), y(0), component(Real), antenna() {}
    GainNameEntry(const GainNameEntry& source)
        : index(source.index),
          x(source.x),
          y(source.y),
          component(source.component),
          antenna(source.antenna) {}
    void operator=(const GainNameEntry& source) {
      index = source.index;
      x = source.x;
      y = source.y;
      component = source.component;
      antenna = source.antenna;
    }
  };

  explicit ParmTable(const std::string& path) : _path(path) { readNames(); }

  std::set<std::string> GetAntennas() const {
    std::set<std::string> antennas;
    for (GainNameEntryMap::const_iterator i = _nameEntries.begin();
         i != _nameEntries.end(); ++i) {
      const GainNameEntry& entry = i->second;
      antennas.insert(entry.antenna);
    }
    return antennas;
  }

  TimeFrequencyData Read(const std::string& antenna) {
    Logger::Debug << "Reading antenna " << antenna << "\n";

    // find the nameid's that we need to select
    const int r00 = FindEntry(0, 0, GainNameEntry::Real, antenna).index,
              r11 = FindEntry(1, 1, GainNameEntry::Real, antenna).index,
              i00 = FindEntry(0, 0, GainNameEntry::Imaginary, antenna).index,
              i11 = FindEntry(1, 1, GainNameEntry::Imaginary, antenna).index;
    Logger::Debug << "Names: r00=" << r00 << ", "
                  << "r11=" << r11 << ", "
                  << "i00=" << i00 << ", "
                  << "i11=" << i11 << "\n";

    casacore::Table table(_path);

    // Construct the images
    unsigned width, height;
    getImageDimensions(table, width, height, r00, r11, i00, i11);
    Image2DPtr xxReal = Image2D::CreateZeroImagePtr(width, height),
               yyReal = Image2D::CreateZeroImagePtr(width, height),
               xxImag = Image2D::CreateZeroImagePtr(width, height),
               yyImag = Image2D::CreateZeroImagePtr(width, height);

    // Read data
    casacore::ROScalarColumn<unsigned int> nameIdColumn(table, "NAMEID");
    casacore::ROScalarColumn<double> startX(table, "STARTX"),
        startY(table, "STARTY");
    casacore::ROArrayColumn<double> values(table, "VALUES");

    int xPos = 0, yPos = 0;
    // double currentX=startX(0);
    double currentY = startY(0);
    unsigned r00Count = 0, r11Count = 0, i00Count = 0, i11Count = 0;
    unsigned curXShape = 0;
    unsigned componentMatches = 0;
    for (unsigned row = 0; row < table.nrow(); ++row) {
      int nameId = nameIdColumn(row);
      if (nameId == r00 || nameId == r11 || nameId == i00 || nameId == i11) {
        Image2DPtr destImage;
        if (nameId == r00) {
          destImage = xxReal;
          ++r00Count;
        } else if (nameId == r11) {
          destImage = yyReal;
          ++r11Count;
        } else if (nameId == i00) {
          destImage = xxImag;
          ++i00Count;
        } else if (nameId == i11) {
          destImage = yyImag;
          ++i11Count;
        }

        const unsigned curYShape = values.shape(row)[1];
        const unsigned xShape = values.shape(row)[0];
        if (xShape > curXShape) curXShape = xShape;
        Logger::Debug << "Image has size " << xShape << " x " << curYShape
                      << '\n';
        const casacore::Array<double> valueArray = values(row);
        casacore::Array<double>::const_iterator vIter = valueArray.begin();
        for (unsigned y = 0; y < curYShape; ++y) {
          for (unsigned x = 0; x < xShape; ++x) {
            destImage->SetValue(yPos + y, xPos + x, *vIter);
            ++vIter;
          }
        }

        ++componentMatches;
        if (componentMatches >= 4) {
          if (startY(row) < currentY) {
            xPos += curXShape;
            yPos = 0;
            curXShape = 0;
          } else {
            yPos += curYShape;
          }
          // currentX=startX(row);
          currentY = startY(row);
          componentMatches = 0;
        }
      }
    }
    Logger::Debug << "Counts: r00=" << r00Count << ", "
                  << "r11=" << r11Count << ", "
                  << "i00=" << i00Count << ", "
                  << "i11=" << i11Count << "\n";
    return TimeFrequencyData(aocommon::Polarization::XX, xxReal, xxImag,
                             aocommon::Polarization::YY, yyReal, yyImag);
  }

  const GainNameEntry& FindEntry(int x, int y, enum GainNameEntry::Component c,
                                 const std::string& antenna) const {
    for (GainNameEntryMap::const_iterator i = _nameEntries.begin();
         i != _nameEntries.end(); ++i) {
      const GainNameEntry& entry = i->second;
      if (entry.x == x && entry.y == y && entry.component == c &&
          entry.antenna == antenna) {
        return entry;
      }
    }
    throw std::runtime_error("Entry not found");
  }

 private:
  void readNames() {
    casacore::Table namesTable;
    if (_path.size() > 0 && *_path.rbegin() != '/')
      namesTable = casacore::Table(_path + "/NAMES");
    else
      namesTable = casacore::Table(_path + "NAMES");

    casacore::ROScalarColumn<casacore::String> nameColumn(namesTable, "NAME");
    for (unsigned i = 0; i != namesTable.nrow(); ++i) {
      std::string name = nameColumn(i);
      addName(i, name);
    }
  }

  void getImageDimensions(casacore::Table& table, unsigned& width,
                          unsigned& height, int r00, int /*r11*/, int /*i00*/,
                          int /*i11*/) {
    casacore::ROScalarColumn<unsigned int> nameIdColumn(table, "NAMEID");
    casacore::ROScalarColumn<double> startX(table, "STARTX"),
        startY(table, "STARTY");
    casacore::ROArrayColumn<double> values(table, "VALUES");

    int maxX = 0, yPos = 0;
    int maxY = 0;
    unsigned matches = 0;
    unsigned curXShape = 0;
    double currentX = startX(0), currentY = startY(0);
    for (unsigned row = 0; row < table.nrow(); ++row) {
      int nameId = nameIdColumn(row);
      if (nameId == r00) {
        const unsigned curYShape = values.shape(row)[1];
        if (values.shape(row)[0] > (int)curXShape)
          curXShape = values.shape(row)[0];
        if (startX(row) < currentX)
          throw std::runtime_error("Table is not correctly ordered");
        yPos += curYShape;
        if (startY(row) < currentY) {
          maxX += curXShape;
          curXShape = 0;
          if (yPos > maxY) maxY = yPos;
          yPos = 0;
        }

        currentX = startX(row);
        currentY = startY(row);
        ++matches;
      }
    }
    maxX += curXShape;
    if (yPos > maxY) maxY = yPos;

    width = maxY;
    height = maxX;
    Logger::Debug << "Rows in table: " << table.nrow()
                  << "\n"
                     "Matching rows: "
                  << matches
                  << "\n"
                     "Number of blocks: "
                  << maxX << " x " << maxY
                  << "\n"
                     "Image size: "
                  << width << " x " << height << "\n";
  }

  void addName(unsigned index, const std::string& line) {
    size_t d1 = line.find(':');
    std::string type = line.substr(0, d1);
    if (type == "Gain") {
      GainNameEntry entry;
      size_t d2 = line.find(':', d1 + 1), d3 = line.find(':', d2 + 1),
             d4 = line.find(':', d3 + 1);
      entry.index = index;
      entry.x = atoi(line.substr(d1 + 1, d2 - d1 - 1).c_str());
      entry.y = atoi(line.substr(d2 + 1, d3 - d2 - 1).c_str());
      std::string component = line.substr(d3 + 1, d4 - d3 - 1);
      if (component == "Real")
        entry.component = GainNameEntry::Real;
      else if (component == "Imag")
        entry.component = GainNameEntry::Imaginary;
      else
        throw std::runtime_error("Incorrect complex component type given");
      entry.antenna = line.substr(d4 + 1);
      _nameEntries.insert(std::pair<unsigned, GainNameEntry>(index, entry));
    }
  }

  std::string _path;
  typedef std::map<unsigned, GainNameEntry> GainNameEntryMap;
  GainNameEntryMap _nameEntries;
};

#endif
