/*********************************************************************
 * This file is part of the PRAPI library.
 *
 * Copyright (C) 2001 Topi Mäenpää and Jaakko Viertola
 * All rights reserved.
 *
 * This program is free software. You can redistribute and/or modify
 * it under the terms of the free software licence found in the
 * accompanying file "COPYING". The licence terms must always be
 * redistributed with this source file. The above copyright notice
 * must be reproduced in all modified and unmodified copies of this
 * source file.
 *
 * $Revision: 1.26 $
 *********************************************************************/

#ifndef _IMAGE_H
#define _IMAGE_H

#include "ConvolutionMask.h"
#include "Color.h"
#include "RasterCodec.h"
#include <Matrix.h>
#include <Math.h>
#include <MatrixCodec.h>
#include <string>

namespace prapi
{
	/**
	 * A common superclass for all exceptions that can be thrown when
	 * handling images.
	 **/
	class ImageException : public util::MatrixException
	{
	public:
		ImageException(std::string message) : MatrixException(message) {}
	};

	/**
	 * Four different ways of handling the border of an matrix when
	 * convolving.
	 * <ul>
	 * <li>BORDER_PAD_WITH_ZEROS - preserve the matrix size by padding the border with zeros
	 * <li>BORDER_REFLECT - preserve the matrix size by reflecting border pixels
	 * <li>BORDER_CIRCULAR - preserve the matrix size by treating it circularly (or spherically, whatever)
	 * <li>BORDER_CROP - the only correct way: throw away the pixels whose value isn't completely defined
	 * </ul>
	 **/
	enum BorderAction { BORDER_PAD_WITH_ZEROS, BORDER_REFLECT, BORDER_CIRCULAR, BORDER_CROP };
	
	/**
	 * Image is a static class which includes funtions for image
	 * analysis. 
	 **/
	class Image
	{
	public:
		/**
		 * Perform a two-dimensional convolution on the matrix using the
		 * given convolution mask.
		 * @param mat The matrix which wanted to operate.
		 * @param mask the convolution mask
		 * @param borderAction how to handle the borders
		 * @return the result of the convolution
		 **/
		template <class T,class U> static util::Matrix<T> convolve(const util::Matrix<T>& mat, const ConvolutionMask<U>& mask,
																															 BorderAction borderAction=BORDER_CROP)throw (ImageException&);

		/**
		 * Split an matrix into sub-matrices. The maximum number of
		 * sub-matrices is taken starting from the given upper left corner.
		 * The sub-matrices are stored in a list in horizontal raster-scan
		 * order.
		 *
		 * @param mat The matrix wanted to split.
		 * @param width the width of the sub-matrices
		 * @param height the height of the sub-matrices
		 * @param startx the x coordinate for the start position (upper left corner)
		 * @param starty the y coordinate for the start position (upper left corner)
		 **/
		template <class T> static util::List<util::Matrix<T> > split(const util::Matrix<T>& mat,int width, int height, int startx = 0, int starty = 0);
		
		/**
		 * Split an matrix into sub-matrices. The maximum number of
		 * sub-matrices is taken starting from the first possible left corner,
		 * which means first place where are 1 in the binary matrix.
		 * The sub-matrices are stored in a list in horizontal raster-scan
		 * order.
		 *
		 * @param mat The matrix wanted to split.
		 * @param width the width of the sub-matrices
		 * @param height the height of the sub-matrices
		 * @param mat The binary image which indictaes the placment of
		 *            areas to split.
		 * @param coordinates The list of strings where the coordinates will be saved.
		 *                    The order is the same as in matrices.
		 * @param value The value from which value areas are wanted. If -1
		 *              all nonzero areas will be split.
		 **/
		template<class T, class U> static util::List<util::Matrix<T> > split(const util::Matrix<T>& mat, const util::Matrix<U>& mask,
																																				 int width, int height, util::List<std::string>& coordinates,U value=-1)
			throw (ImageException&);

		/**
		 * Scale the values in a matrix to the range [0,max].
		 **/
		template <class T> static util::Matrix<T> scaleRange(const util::Matrix<T>& input, T max);

		/**
		 * Invert the values in an image. The inversion is performed by
		 * finding the maximum value, and subtracting all pixel values
		 * from it.
		 **/
		template <class T> static util::Matrix<T> invert(const util::Matrix<T>& image);
		
		/**
		 * Perform a mask operation on an image. Sets all pixels in mat to
		 * zero that have a corresponding zero in mask. Mat and mask must
		 * be of equal sizes.
		 *
		 * @param mat The image to be masked.
		 * @param mask The mask, with non-zero values on pixels that should be preserved.
		 **/
		template <class T,class U> static void mask(util::Matrix<T>& mat, const util::Matrix<U>& mask);

		/**
		 * Get a color channel from a multi-channel image.
		 *
		 * @param c The index of the color channel.
		 * @param mat A color image.
		 **/
		template <class T, int comps> static util::Matrix<T> getChannel(int c, const util::Matrix<Color<T,comps> >& mat);

		/**
		 * Set a color channel in a multi-channel image.
		 *
		 * @param c The index of the color channel.
		 * @param chn the color channel (whose size must be equal to the color image)
		 * @param mat the color image.
		 * @exception ImageException& if channel and image sizes differ
		 **/
		template <class T, int comps> static void setChannel(int c, const util::Matrix<T>& chn,
																												 util::Matrix<Color<T,comps> >& mat)
			throw (ImageException&);

		/**
		 * Normalize a color image. Return an image where each color
		 * channel has been divided by the sum of all channels.
		 **/
		template <class T, int comps> static util::Matrix<Color<double,comps> > normalize(const util::Matrix<Color<T,comps> >& mat);

		/**
		 * Premultiply each color in a multi-channel color image by a
		 * matrix. Return a new image with each color value transformed by
		 * the linear tranformation.
		 **/
		template <class T, class U, int comps> static util::Matrix<Color<double,comps> > transform(const util::Matrix<Color<U,comps> >& mat,
																																															 const util::Matrix<T>& tr)
			throw (util::MatrixException&);

		/**
		 * Get a one-channel average (intensity) image from a
		 * multi-channel color image. The returned image is of the same
		 * size as the input image, and contains the average of each color
		 * channel at each pixel.
		 **/
		template <class T, int comps> static util::Matrix<T> average(const util::Matrix<Color<T,comps> >& mat);

		/**
		 * Convert a color image from the CIE XYZ color space to CIE LAB.
		 *
		 * @param mat the color image to be converted
		 * @param whitePoint the XYZ coordinates of the white point of the
		 * image equipment.
		 *
		 * @see ColorTransform::XYZtoCIELAB
		 **/
		static util::Matrix<Color<double,3> > XYZToCIELAB(const util::Matrix<Color<double,3> >& mat,
																											const Color<double,3>& whitePoint);

		/**
		 * Convert an image in the RGB color space to HSV.
		 *
		 * @see ColorTransform::RGBtoHSV
		 **/
		template <class T> static util::Matrix<Color<T,3> > RGBToHSV(const util::Matrix<Color<T,3> >& mat);

		/**
		 * Convert an image in the HSV color space to RGB.
		 *
		 * @see ColorTransform::HSVtoRGB
		 **/
		template <class T> static util::Matrix<Color<T,3> > HSVToRGB(const util::Matrix<Color<T,3> >& mat);
	};

	template <class T> util::Matrix<T> Image::scaleRange(const util::Matrix<T>& mat, T max)
	{
		T origMin(0), origMax(0);
		util::Math::minAndMax(mat, origMin, origMax);
		if (origMin == origMax)
			return mat;
		double scaleFactor = double(max) / double(origMax-origMin);
		int rows = mat.getRows(), columns = mat.getColumns();
		int size = rows*columns;
		util::Matrix<T> result(rows,columns);
		T* tData = result.getData();
		const T* sData = mat.getData();
		for (int i=size; i--; tData++, sData++)
			*tData = T(scaleFactor * (*sData-origMin));
		return result;
	}

	template <class T> util::Matrix<T> Image::invert(const util::Matrix<T>& mat)
	{
		T max = util::Math::max(mat);
		int size = mat.getRows()*mat.getColumns();
		util::Matrix<T> result(mat.getRows(), mat.getColumns(), false);
		const T* sData = mat.getData();
		T* tData = result.getData();
		for (int i=size; i--; sData++,tData++)
			*tData = max - *sData;
		return result;
	}


	template <class T, int comps> util::Matrix<Color<double, comps> > Image::normalize(const util::Matrix<Color<T,comps> >& mat)
	{
		int size = mat.getRows()*mat.getColumns();
		util::Matrix<Color<double,comps> > result(mat.getRows(), mat.getColumns(), false);
		const Color<T,comps>* sData = mat.getData();
		Color<double,comps>* tData = result.getData();
		for (int i=size; i--; sData++,tData++)
			*tData = ColorTransform::normalize(*sData);
		return result;
	}

	template <class T, class U, int comps> util::Matrix<Color<double,comps> > Image::transform(const util::Matrix<Color<U,comps> >& mat,
																																														 const util::Matrix<T>& tr)
		throw (util::MatrixException&)
	{
		int size = mat.getRows()*mat.getColumns();
		util::Matrix<Color<double,comps> > result(mat.getRows(), mat.getColumns(), false);
		const Color<U,comps>* sData = mat.getData();
		Color<double,comps>* tData = result.getData();
		for (int i=size; i--; sData++,tData++)
			*tData = tr * *sData;
		return result;
	}

	template <class T, int comps> util::Matrix<T> Image::average(const util::Matrix<Color<T,comps> >& mat)
		{
			int r = mat.getRows(), c = mat.getColumns();
			util::Matrix<T> result(r,c);
			T* tData = result.getData();
			const Color<T,comps>* sData = mat.getData();
			for (int i=r*c; i--; sData++, tData++)
				{
					T sum((*sData)[0]);
					for (int c=1; c<comps; c++)
						sum += (*sData)[c];
					*tData = sum/comps;
				}
			return result;
		}

	template <class T> util::Matrix<Color<T,3> > Image::RGBToHSV(const util::Matrix<Color<T,3> >& mat)
	{
		int size = mat.getRows()*mat.getColumns();
		util::Matrix<Color<T,3> > result(mat.getRows(), mat.getColumns(), false);
		const Color<T,3>* sData = mat.getData();
		Color<T,3>* tData = result.getData();
		for (int i=size; i--; sData++,tData++)
			*tData = ColorTransform::RGBToHSV(*sData);
		return result;
	}

	template <class T> util::Matrix<Color<T,3> > Image::HSVToRGB(const util::Matrix<Color<T,3> >& mat)
	{
		int size = mat.getRows()*mat.getColumns();
		util::Matrix<Color<T,3> > result(mat.getRows(), mat.getColumns(), false);
		const Color<T,3>* sData = mat.getData();
		Color<T,3>* tData = result.getData();
		for (int i=size; i--; sData++,tData++)
			*tData = ColorTransform::HSVToRGB(*sData);
		return result;
	}

	template <class T, class U> void Image::mask(util::Matrix<T>& mat, const util::Matrix<U>& mask)
	{
		int size = mat.getRows()*mat.getColumns();
		int sizeMask = mask.getRows()*mask.getColumns();
		
		if (size != sizeMask)
			throw ImageException("Image::mask(Matrix<T>&, const Matrix<U>&): Matrix sizes differ.");
		
		const U* maskData = mask.getData();
		T* data = mat.getData();
		
		for(int i=0;i<size;i++,maskData++,data++)
			if(*maskData == U(0))*data=T(0);
	}

	template <class T> util::List<util::Matrix<T> > Image::split(const util::Matrix<T>& mat,
																															 int width, int height, int startx, int starty)
	{
		util::List<util::Matrix<T> > result;
		int rows = mat.getRows();
		int cols = mat.getColumns();
		if (startx + width <= cols && starty + height <= rows)
			{
				rows = (rows-starty)/height;
				cols = (cols-startx)/width;
				result.setCapacity(rows*cols);
				for (int r=0;r<rows;r++)
					for (int c=0;c<cols;c++)
						{
							result += mat(r*height+starty,c*width+startx,height,width);
						}
			}
		return result;
	}

template <class T, class U> util::List<util::Matrix<T> > Image::split(const util::Matrix<T>& mat, const util::Matrix<U>& mask,
																														int width, int height, util::List<std::string>& coordinates,U value)
		throw (ImageException&)
	{
		util::List<util::Matrix<T> > result;
		int r=0,c=0;
		int rows = mat.getRows();
		int cols = mat.getColumns();
		if(rows != mask.getRows() || cols != mask.getColumns())
			throw ImageException("Image<T>::split(const Matrix<U>&, const Matrix<U>&, int, int, List<string>&, U): Matrix sizes differ.");

		std::string widthHeight(" "+util::String::toString(width)+" "+util::String::toString(height));

		int widthInc=width;
		int heightInc=height;
		width--;
		height--;
		bool doSplit = true;
		bool splitDone = false;
		// find first column what is nonzero/value in binary mask.
		if(value == -1)
			{
				value=0;
				for(c=0;c<cols-width;c++)
					{
						splitDone=false;
						for(r=0;r<rows-height;r++)
							if(mask(r,c) != 0)
								{ // check if the split can be done
									doSplit =true;
									for(int i=0;i<=width;i++)if(mask(r,c+i) == 0)doSplit=false;
									if(doSplit)for(int i=0;i<=width;i++)if(mask(r+height,c+i) == 0)doSplit=false;
									if(doSplit)for(int i=0;i<=height;i++)if(mask(r+i,c) == 0)doSplit=false;
									if(doSplit)for(int i=0;i<=height;i++)if(mask(r+i,c+width) == 0)doSplit=false;
									//check if split can be done.
									if(doSplit)
										{
											result += mat(r,c,heightInc,widthInc);
											coordinates += (String::toString(r)+" "+String::toString(c)+widthHeight);
											r+=height; // move the row index
											splitDone = true;
										}
								}//if mask(r,c)!=0
						if(splitDone)c+=width;//if split is done remember to move column index.
					}//for outer
			}// if value == -1
		else
			{
				cerr << "starting else"<<endl;
				for(c=0;c<cols-width;c++)
					{
						splitDone=false;
						for(r=0;r<rows-height;r++)
							if(mask(r,c) == value)
								{ // check if the split can be done
									doSplit =true;
									for(int i=0;i<=width;i++)if(mask(r,c+i) != value)doSplit=false;
									if(doSplit)for(int i=0;i<=width;i++)if(mask(r+height,c+i) != value)doSplit=false;
									if(doSplit)for(int i=0;i<=height;i++)if(mask(r+i,c) != value)doSplit=false;
									if(doSplit)for(int i=0;i<=height;i++)if(mask(r+i,c+width) != value)doSplit=false;
									//check if split can be done.
									if(doSplit)
										{
											result += mat(r,c,heightInc,widthInc);
											coordinates += (String::toString(r)+" "+String::toString(c)+widthHeight);
											r+=height; // move the row index
											splitDone = true;
										}
								}//if mask(r,c)!=0
						if(splitDone)c+=width;//if split is done remember to move column index.
					}//for outer
			}// else
		return result;
	}

	template <class T, class U> util::Matrix<T> Image::convolve(const util::Matrix<T>& mat, const ConvolutionMask<U>& mask,
																															BorderAction borderAction)
		throw (ImageException&)
	{
		int r,c;
		U divisor = mask.getDivisor();
		int originX = mask.getOrigin().x, originY = mask.getOrigin().y;
		int maskWidth = mask.getColumns(), maskHeight = mask.getRows();
		int rows = mat.getRows();
		int cols = mat.getColumns();
		
		if (maskWidth > cols || maskHeight > rows)
			throw ImageException("Image::convolve(const Matrix<T>&, const ConvolutionMast<U>&, BorderAction): Mask size exceeds matrix size.");
		if (originX >= maskWidth || originY >= maskHeight ||
				originX < 0 || originY < 0)
			throw ImageException("Image::convolve(const Matrix<T>&, const ConvolutionMast<U>&, BorderAction): "
													 "The origin of the convolution mask is outside of the mask.");
		
		util::Matrix<T> result;

		if (borderAction == BORDER_CROP)
			{
				result.setSize(rows-maskHeight+1,cols-maskWidth+1);
				//cerr << "Filtering cropped\n";
				AllItems(r,c,result)
					{
						T sum = 0;
						for (int y=0;y<mask.getRows();y++)
							for (int x=0;x<mask.getColumns();x++)
								sum += T(mat(r+y,c+x)*mask(y,x));
						result(r,c) = T(sum / divisor);
					}
				//cerr << "Done\n";
			}
		else
			{
				util::Matrix<T> tmp(rows+maskHeight-1,cols+maskWidth-1);
				result.setSize(rows,cols);

				AllItems(r,c,mat)
					tmp(r+originY,c+originX) = mat(r,c);					
				
				switch (borderAction)
					{
					case BORDER_REFLECT:
						for (c=0;c<cols;c++)
							for (r=0;r<originY;r++)
								tmp(originY-r-1,c+originX) = tmp(originY+r,c+originX);

						for (c=0;c<cols;c++)
							for (r=0;r<maskHeight-originY-1;r++)
								tmp(originY+rows+r,c+originX) = tmp(originY+rows-r-1,c+originX);

						for (r=0;r<rows+maskHeight-1;r++)
							for (c=0;c<originX;c++)
								tmp(r,originX-c-1) = tmp(r,originX+c);
						
						for (r=0;r<rows+maskHeight-1;r++)
							for (c=0;c<originX;c++)
								tmp(r,originX+cols+c) = tmp(r,originX+cols-c-1);

						break;

					case BORDER_CIRCULAR:
						for (c=0;c<cols;c++)
							for (r=0;r<originY;r++)
								tmp(r,c+originX) = tmp(r+rows,c+originX);

						for (c=0;c<cols;c++)
							for (r=0;r<maskHeight-originY-1;r++)
								tmp(originY+rows+r,c+originX) = tmp(originY+r,c+originX);

						for (r=0;r<rows+maskHeight-1;r++)
							for (c=0;c<originX;c++)
								tmp(r,c) = tmp(r,cols+c);
						
						for (r=0;r<rows+maskHeight-1;r++)
							for (c=0;c<originX;c++)
								tmp(r,originX+cols+c) = tmp(r,originX+c);
						
						break;

					default:
						break;
					}

				AllItems(r,c,result)
					{
						T sum = 0;
						for (int y=0;y<mask.getRows();y++)
							for (int x=0;x<mask.getColumns();x++)
								sum += T(tmp(r+y,c+x)*mask(y,x));
						result(r,c) = T(sum / divisor);
					}
			}
		return result;
	}
	
	template <class T,int comps> util::Matrix<T> Image::getChannel(int c,const util::Matrix<Color<T,comps> >& mat)
	{
		int rows = mat.getRows();
		int cols = mat.getColumns();
		util::Matrix<T> result(rows,cols);
		const Color<T,comps>* data = mat.getData();
		T* targetData = result.getData();
		for (int i=rows*cols; i--; data++, targetData++)
			*targetData = (*data)[c];
		return result;
	}

	template <class T, int comps> void Image::setChannel(int c, const util::Matrix<T>& chn,
																											 util::Matrix<Color<T,comps> >& mat)
		throw (ImageException&)
	{
		int rows = mat.getRows();
		int cols = mat.getColumns();
		if (chn.getRows() != rows || chn.getColumns() != cols)
			throw ImageException("Image::setChannel(int,Matrix&,Matrix&): Color channel and target image are of different size.");
		if (c >= comps || c < 0)
			throw ImageException("Image::setChannel(int,Matrix&,Matrix&): Cannot set color channel " + util::String::toString(c) +
													 " on a " + util::String::toString(comps) + "-channel image.");

		const T* chnData = chn.getData();
		Color<T,comps>* targetData = mat.getData();
		for (int i=rows*cols; i--; chnData++, targetData++)
			(*targetData)[c] = *chnData;
	}
}

#endif
