#pragma once

#include <string>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <cmath>
#include <cassert>
#include <cstring>
#include <cstdlib>

#include "pnm.h"

using namespace std;


//
//
// Point
//

template <typename T>
struct TPoint {
  T x;
  T y;

  TPoint(T a, T b)
  : x(a)
  , y(b)
  {}

  TPoint& operator+=(TPoint const& rhs)
  {
    x += rhs.x;
    y += rhs.y;
    return *this;
  }

  TPoint& operator-=(TPoint const& rhs)
  {
    x -= rhs.x;
    y -= rhs.y;
    return *this;
  }

  TPoint& operator*=(TPoint const& rhs)
  {
    x *= rhs.x;
    y *= rhs.y;
    return *this;
  }

  TPoint& operator/=(TPoint const& rhs)
  {
    x /= rhs.x;
    y /= rhs.y;
    return *this;
  }
};

typedef TPoint<int64_t> Point;
typedef TPoint<double> DPoint; // absolute point, can be negative
template<typename Elem, typename Traits, typename T>

std::basic_ostream<Elem, Traits>& operator << (std::basic_ostream<Elem, Traits>& o, TPoint<T> const& p)
{
  o << "(" << p.x << ", " << p.y << ")";
  return o;
}



//
//
// Pixel
//

typedef uint16_t pvalue_t; // pixel value type



//
//
// Image
//

struct Image {
  unsigned int width;
  unsigned int height;
  pvalue_t* buffer;
  pnm::Format type;
  size_t pixel_size;

  Image();
  Image(unsigned int w, unsigned int h, pnm::Format type);
  Image(string const& path);
  virtual ~Image();

  bool save(string const& path) const;

  protected:
    virtual bool read_body(std::ifstream& istr);
    virtual bool write_body(std::ofstream& ostr) const;
};

template<unsigned int W, unsigned int H>
struct TiledImage : public Image {
  pvalue_t* tiles;
  unsigned int static const tile_w = W;
  unsigned int static const tile_h = H;

  // two lines overlap, bottom + right
  unsigned int static const tile_size = (W + 1) * (H + 1);
  unsigned int nb_col_tile;
  unsigned int nb_row_tile;

  TiledImage()
  : Image()
  , tiles(NULL)
  , nb_col_tile(0)
  , nb_row_tile(0)
  {}

  ~TiledImage()
  {
    delete [] tiles;
  }

  TiledImage(unsigned int w, unsigned int h)
  {
    allocate_memory(w, h);
  }

  TiledImage(string const& path)
  : TiledImage()
  {
      ifstream is(path);
      if (!is.is_open())
      {
          cerr << "Cannot open file '" << path << "'" << endl;
          abort();
      }

      if (!pnm::read_header(is, this->type, this->width, this->height))
      {
          cerr << "Invalid header." << endl;
          abort();
      }

      if (!this->read_body(is))
      {
          // TODO: delete tiles
          cerr << "Invalid header." << endl;
          abort();
      }
  }

  pvalue_t const*
  get_tile(unsigned int index) const
  {
    if (index >= nb_col_tile * nb_row_tile)
      return nullptr;

    return tiles + index * tile_size * pixel_size;
  }

  pvalue_t*
  get_tile(unsigned int index)
  {
    if (index >= nb_col_tile * nb_row_tile)
      return nullptr;

    return tiles + index * tile_size * pixel_size;
  }

  pvalue_t*
  access_pixel(unsigned int x, unsigned int y)
  {
    if (x >= width || y >= height)
      return nullptr;

    unsigned int const tile_width = (tile_w + 1) * pixel_size;

    unsigned int const tile_index = (y / tile_h) * nb_col_tile + (x / tile_w);
    pvalue_t* tile = this->get_tile(tile_index);
    unsigned int const tile_j = y % tile_h;
    unsigned int const tile_i = x % tile_w;
    return tile + tile_j * tile_width + (tile_i * pixel_size);
  }

  pvalue_t const*
  access_pixel(unsigned int x, unsigned int y) const
  {
    if (x >= width || y >= height)
      return nullptr;

    unsigned int const tile_width = (tile_w + 1) * pixel_size;

    unsigned int const tile_index = (y / tile_h) * nb_col_tile + (x / tile_w);
    const pvalue_t* tile = this->get_tile(tile_index);
    unsigned int const tile_j = y % tile_h;
    unsigned int const tile_i = x % tile_w;
    return tile + tile_j * tile_width + (tile_i * pixel_size);
  }

  void
  print_tile(unsigned int index) const
  {
    cout << "Tile[" << index << "]" << endl;
    pvalue_t const* tile = this->get_tile(index);
    unsigned int const tile_width = (tile_w + 1) * pixel_size;
    for (unsigned int j = 0; j < tile_h + 1; ++j)
    {
      for (unsigned int i = 0; i < tile_w + 1; ++i)
      {
        if (i != 0)
          cout << ", ";
        pvalue_t const* p = tile + j * tile_width + i * pixel_size;
        cout << (int) *p << " " << (int) *(p + 1) << " " << (int) *(p + 2);

      }
      cout << endl;
    }
    cout << endl;
  }

  void fill_overlap()
  {
    unsigned int const tile_width = (W + 1) * pixel_size;

    for (int j = nb_row_tile - 1; j >= 0; --j)
      for (unsigned int i = 0; i < nb_col_tile; ++i)
      {
        // copy last line overlap
        if (j != (int) nb_row_tile - 1)
        {
          pvalue_t const* tile_src = this->access_pixel(i * W, (j + 1) * H);
          pvalue_t* tile_dst = this->access_pixel(i * W, j * H);
          tile_dst += H * tile_width;
          memcpy(tile_dst, tile_src, tile_width * sizeof (pvalue_t));
        }

        // copy last col overlap
        if (i != nb_col_tile - 1)
        {
          pvalue_t* tile_src = this->get_tile(i + 1 + j * nb_col_tile);
          pvalue_t* tile_dst = this->get_tile(i + j * nb_col_tile);
          tile_dst += W * pixel_size;
          for (unsigned int y = 0; y < H; ++y)
          {
            memcpy(tile_dst, tile_src, pixel_size * sizeof (pvalue_t));
            tile_src += tile_width;
            tile_dst += tile_width;
          }
        }
      }
  }


  protected:
    void allocate_memory(unsigned int w, unsigned int h)
    {
      width = w;
      height = h;

      nb_col_tile = width / tile_w;
      if (width % tile_w != 0)
        ++nb_col_tile;

      nb_row_tile = height / tile_h;
      if (height % tile_h != 0)
        ++nb_row_tile;

      unsigned int const nb_tiles = nb_col_tile * nb_row_tile;
      tiles = new pvalue_t[nb_tiles * tile_size * pixel_size];
      memset(tiles, 0, nb_tiles * tile_size * pixel_size * sizeof (pvalue_t));
    }

    virtual bool read_body(std::ifstream& istr) override
    {
      this->allocate_memory(width, height);

      // Pixel loading
      for (unsigned int j = 0; j < height; ++j)
        for (unsigned int i = 0; i < width; ++i)
        {
          pvalue_t* tile = this->access_pixel(i, j);
          tile[0] = istr.get();
          tile[1] = istr.get();
          tile[2] = istr.get();

          tile += pixel_size;
        }

      this->fill_overlap();

      return true;
    }

    virtual bool write_body(std::ofstream& ostr) const override
    {
      for (unsigned int j = 0; j < height; ++j)
        for (unsigned int i = 0; i < width; ++i)
        {
          pvalue_t const* tile = this->access_pixel(i, j);
          ostr << (char) tile[0];
          ostr << (char) tile[1];
          ostr << (char) tile[2];

          tile += pixel_size;
        }

      return true;
    }

};
