/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "bagel/baglib/pan_bitmap.h"
#include "bagel/baglib/bagel.h"
#include "bagel/baglib/paint_table.h"

namespace Bagel {

const CBofRect CBagPanBitmap::getMaxView(CBofSize s) {
	CBofRect r;

	if (_bPanorama) {
		int h = 3 * (int)((double)width() / (MAX_DIV_VIEW * 4));
		if (h > height())
			h = height();

		r.setRect(0, 0, (int)(width() / MAX_DIV_VIEW - 2), h - 1);

	} else {
		r.setRect(0, 0, width() - 1, height() - 1);
	}

	if (s.cx > 0 && r.right > s.cx) {
		// r.left = s.cx;
		r.right = s.cx - 1;
	}

	if (s.cy > 0 && r.bottom > s.cy) {
		r.bottom = s.cy - 1;
	}

	return r;
}

CBagPanBitmap::CBagPanBitmap(const char *pszFileName, CBofPalette *pPalette, const CBofRect &xViewSize) :
		CBofBitmap(pszFileName, pPalette, true) {
	int nW = width();
	int nH = height();

	_bPanorama = false;
	_pCosineTable = nullptr;
	_bActiveScrolling = false; // The scrolling is not active
	_xDirection = kDirNONE;    // Direction is not moving
	_xFOVAngle = 0;
	_nCorrWidth = 0;
	_nNumDegrees = 0;


	if (nW && nH) {
		CBofRect xMaxViewSize(0, 0, nW - 1, nH - 1);
		if (nW > 1000) {
			xMaxViewSize.left = (long)(nW / MAX_DIV_VIEW);
			_bPanorama = true;
		}

		pPalette = getPalette();

		if (xViewSize.isRectEmpty())
			_xCurrView = xMaxViewSize;
		else
			_xCurrView = xViewSize;

		if (_xCurrView.width() > xMaxViewSize.width()) {
			_xCurrView.setRect(0, _xCurrView.top, xMaxViewSize.width() - 1, _xCurrView.bottom);
		}

		_xRotateRate.x = (nW - _xCurrView.width()) / 64 + 1;
		_xRotateRate.y = (nH - _xCurrView.height()) / 64 + 1;

		normalizeViewSize();

		// If FOV is set to 0 then unity FOV is assumed (faster redraws)
		// The update parameter is set to false to avoid to update the cosine table as _nCorrWidth isn't set yet
		setFOV(DEF_FOV, false);

		// Initialize _nCorrWidth and generate the cosine table.
		if (_bPanorama)
			setCorrWidth(4, true);
		else
			setCorrWidth(0, true);

		_bIsValid = true;

		return;
	}

	_bIsValid = false;
}

CBagPanBitmap::~CBagPanBitmap() {
	assert(isValidObject(this));

	delete[] _pCosineTable;
	_pCosineTable = nullptr;
}

// This must be updated whenever the size, view size, or correction width changes
void CBagPanBitmap::generateCosineTable() {
	int nWidth = 1 << _nCorrWidth;
	int offset = nWidth >> 1; // This is not really needed just more correction to move angle to center
	int viewWidth = _xCurrView.width();

	_nNumDegrees = (viewWidth >> _nCorrWidth) + 1;

	delete[] _pCosineTable;
	_pCosineTable = new CBofFixed[_nNumDegrees];

	for (int i = 0; i < _nNumDegrees; i++) {
		double inArea = (double)(offset + i * nWidth) / viewWidth;
		_pCosineTable[i] = (CBofFixed)(cos(_xFOVAngle * (-1.0 + 2.0 * inArea)));
	}
}

CBofRect CBagPanBitmap::getWarpSrcRect() {
	int nH2 = height() >> 1;

	return CBofRect(_xCurrView.left,
	                nH2 + (int)(*_pCosineTable * CBofFixed(_xCurrView.top - nH2)),
	                _xCurrView.right,
	                nH2 + (int)(*_pCosineTable * CBofFixed(_xCurrView.bottom - nH2)));
}

CBofPoint CBagPanBitmap::warpedPoint(CBofPoint &xPoint) {
	CBofRect r = getRect();
	int nH2 = height() >> 1;
	int nW = width();
	int nWidth = 1 << _nCorrWidth; // It may no longer be necessary to store corr width as a shift arg
	int nCenter = r.top + nH2;

	int nOffset = (xPoint.x - _xCurrView.left); // nWidth;
	if (nOffset < 0)
		nOffset += nW;
	nOffset /= nWidth;

	if ((nOffset < 0) || ((xPoint.x - _xCurrView.left) > _xCurrView.width()) || (nOffset >= _nNumDegrees))
		return CBofPoint(0, 0);

	CBofFixed srcHeight = _pCosineTable[nOffset];

	return CBofPoint(xPoint.x, nCenter + (int)(srcHeight * CBofFixed(xPoint.y - nH2)));
}

ErrorCode CBagPanBitmap::paintWarped(CBofBitmap *pBmp, const CBofRect &dstRect, const CBofRect &srcRect, const int offset, CBofBitmap *pSrcBmp, const CBofRect &preSrcRect) {
	int nH2 = _nDY >> 1;
	int nWidth = 1 << _nCorrWidth; // It may no longer be necessary to store corr width as a shift arg
	CBofFixed *pSrcHeight = &_pCosineTable[offset >> _nCorrWidth];
	CBofFixed srcTop = preSrcRect.top + srcRect.top - nH2;
	CBofFixed srcBottom = preSrcRect.top + srcRect.bottom - nH2;
	int nCenter = nH2;

	int nTop = nCenter - preSrcRect.top;
	int nRight = (srcRect.left + nWidth) - 1;

	CBofRect PanSrcRect;
	CBofRect WndDstRect(dstRect.left + 0, dstRect.top, (dstRect.left + 0 + nWidth) - 1, dstRect.bottom);

	pBmp->lock();
	pSrcBmp->lock();

	int nIncrement = 1;

	if (nWidth < 4) {
		for (int i = 0; i < dstRect.width(); i += nWidth) {
			// Set the source
			//
			PanSrcRect.setRect(srcRect.left + i,
			                   nTop + (int)(*pSrcHeight * srcTop),
			                   nRight + i,
			                   nTop + (int)(*pSrcHeight * srcBottom));

			pSrcBmp->paint(pBmp, &WndDstRect, &PanSrcRect);

			WndDstRect.left = WndDstRect.right + 1;
			WndDstRect.right = WndDstRect.right + nWidth;
			pSrcHeight += nIncrement;
		}
	} else if (nWidth == 4) {
		int tableSlot = srcRect.top + preSrcRect.top;
		int stripNumber = 0;

		for (int i = 0; i < dstRect.width(); i += nWidth, stripNumber++) {
			// Set the source
			PanSrcRect.setRect(srcRect.left + i,
			                   STRIP_POINTS[tableSlot][stripNumber]._top,
			                   nRight + i,
			                   STRIP_POINTS[tableSlot][stripNumber]._bottom);

			pSrcBmp->paintStretch4(pBmp, &WndDstRect, &PanSrcRect);

			WndDstRect.left = WndDstRect.right + 1;
			WndDstRect.right = WndDstRect.right + nWidth;
		}
	} else { // nWidth > 4
		for (int i = 0; i < dstRect.width(); i += nWidth) {
			// Set the source
			PanSrcRect.setRect(srcRect.left + i,
			                   nTop + (int)(*pSrcHeight * srcTop),
			                   nRight + i,
			                   nTop + (int)(*pSrcHeight * srcBottom));

			pSrcBmp->paintStretchOpt(pBmp, &WndDstRect, &PanSrcRect, nWidth);

			WndDstRect.left = WndDstRect.right + 1;
			WndDstRect.right = WndDstRect.right + nWidth;
			pSrcHeight += nIncrement;
		}
	}

	pSrcBmp->unlock();
	pBmp->unlock();

	return _errCode;
}

ErrorCode CBagPanBitmap::paintUncorrected(CBofBitmap *pBmp, CBofRect &dstRect) {
	int tmp = _nCorrWidth;
	_nCorrWidth = 0;

	CBofFixed fONE(1);
	CBofFixed fH2(height() / 2);
	CBofFixed fCos(_pCosineTable[0]);
	int nOffset = (int)((fONE - fCos) * fH2);

	dstRect = getCurrView();
	CBofRect tmpRect = dstRect;
	dstRect.top -= nOffset;
	dstRect.bottom += nOffset;

	if (dstRect.top < 0) {
		dstRect.bottom = dstRect.height() - 1;
		dstRect.top = 0;
	}
	if (dstRect.height() >= pBmp->height()) {
		dstRect.bottom = dstRect.top + pBmp->height() - 2;
	}
	_xCurrView = dstRect;

	paint(pBmp);
	_xCurrView = tmpRect;

	_nCorrWidth = tmp;

	return _errCode;
}

ErrorCode CBagPanBitmap::paint(CBofBitmap *pBmp, const CBofPoint xDstOffset) {
	CBofRect dstRect;
	CBofRect srcRect = _xCurrView;
	int nW = width();
	int viewWidth = _xCurrView.width();
	srcRect.right = _xCurrView.left + viewWidth - 1;
	int nOffset = srcRect.right - nW;

	dstRect.top = xDstOffset.y;
	dstRect.bottom = dstRect.top + srcRect.height() - 1;

	// If the right side of the view is the beginning of the panorama
	// paint the un-wrapped side (right) first.
	dstRect.left = xDstOffset.x;
	dstRect.right = xDstOffset.x + viewWidth - 1;

	if (nOffset > 0) {
		CBofRect srcRect2 = srcRect;
		srcRect2.left = 0;
		srcRect2.right = nOffset;
		dstRect.right = xDstOffset.x + viewWidth - 1;
		dstRect.left = dstRect.right - nOffset;

		CBofBitmap::paint(pBmp, &dstRect, &srcRect2);

		srcRect.right = nW - 1;
		dstRect.right = dstRect.left;
		dstRect.left = xDstOffset.x;
	}

	CBofBitmap::paint(pBmp, &dstRect, &srcRect);

	return _errCode;
}

CBagPanBitmap::Direction CBagPanBitmap::updateView() {
	if (_bActiveScrolling) {
		if (_xDirection & kDirLEFT)
			rotateRight();
		else if (_xDirection & kDirRIGHT)
			rotateLeft();

		if (_xDirection & kDirUP)
			rotateUp();
		else if (_xDirection & kDirDOWN)
			rotateDown();

		return _xDirection;
	}

	return kDirNONE;
}

void CBagPanBitmap::setCorrWidth(int nWidth, bool bUpdate) {
	int i = 0;

	while (nWidth >>= 1)
		++i;

	if (i < 6) {
		_nCorrWidth = i;
		if (bUpdate) {
			generateCosineTable();
		}
	}
}

void CBagPanBitmap::rotateRight(int nXRotRate) {
	if (nXRotRate > 0)
		offsetCurrView(CBofPoint(nXRotRate, 0));
	else
		offsetCurrView(CBofPoint(_xRotateRate.x, 0));
}

void CBagPanBitmap::rotateLeft(int nXRotRate) {
	if (nXRotRate > 0)
		offsetCurrView(CBofPoint(-1 * nXRotRate, 0));
	else
		offsetCurrView(CBofPoint(-1 * _xRotateRate.x, 0));
}

void CBagPanBitmap::rotateUp(int nYRotRate) {
	if (nYRotRate > 0)
		offsetCurrView(CBofPoint(0, nYRotRate));
	else
		offsetCurrView(CBofPoint(0, _xRotateRate.y));
}

void CBagPanBitmap::rotateDown(int nYRotRate) {
	if (nYRotRate > 0)
		offsetCurrView(CBofPoint(0, -1 * nYRotRate));
	else
		offsetCurrView(CBofPoint(0, -1 * _xRotateRate.y));
}

void CBagPanBitmap::normalizeViewSize() {
	int nW = width();
	int nH = height();

	if (_bPanorama) {
		// The CurrView can not be more than 0.25Width x Height of the Bitmap
		if ((_xCurrView.width() >= nW / MAX_DIV_VIEW) || (_xCurrView.width() <= 0))
			_xCurrView.right = (long)(_xCurrView.left + nW / MAX_DIV_VIEW - 1);
		if ((_xCurrView.height() >= nH) || (_xCurrView.height() <= 0))
			_xCurrView.bottom = _xCurrView.top + nH - 1;

		// The Base coords of CurrView must exist within the rectangle
		while (_xCurrView.left < 0)
			_xCurrView.offsetRect(nW, 0);
		while (_xCurrView.left >= nW)
			_xCurrView.offsetRect(-1 * nW, 0);
	} else { // Not a panorama
		// The Base coords of CurrView must exist within the rectangle
		if (_xCurrView.left < 0)
			_xCurrView.offsetRect(-_xCurrView.left, 0);
		else if (_xCurrView.right > nW)
			_xCurrView.offsetRect(nW - _xCurrView.right, 0);
	}

	// We never have up and down wrap around
	if (_xCurrView.top < 0)
		_xCurrView.offsetRect(0, -_xCurrView.top);
	if (_xCurrView.bottom >= nH)
		_xCurrView.offsetRect(0, (nH - _xCurrView.bottom) - 1);
}


void CBagPanBitmap::setCurrView(const CBofRect &xCurrView) {
	_xCurrView = xCurrView;
	normalizeViewSize();
}

void CBagPanBitmap::offsetCurrView(const CBofPoint &xOffset) {
	CBofRect xCurrView = _xCurrView;
	xCurrView.offsetRect(xOffset);
	setCurrView(xCurrView);
}

void CBagPanBitmap::setFOV(double degrees, bool bUpdate) {
	_xFOVAngle = degrees / 114.5916558176;
	if (bUpdate) {
		generateCosineTable();
	}
}

void CBagPanBitmap::setViewSize(const CBofSize &xViewSize, bool bUpdate) {
	_xCurrView.right = _xCurrView.left + xViewSize.cx;
	_xCurrView.bottom = _xCurrView.top + xViewSize.cy;
	normalizeViewSize();

	if (bUpdate) {
		generateCosineTable();
	}
}

CBofSize CBagPanBitmap::setUnityViewSize() {
	int w = (int)(width() * _xFOVAngle / 3.14159);
	_xCurrView.setRect(0, _xCurrView.top, w, _xCurrView.bottom);
	generateCosineTable();
	return getViewSize();
}

double CBagPanBitmap::setUnityFOV() {
	setFOV(360.0 * _xCurrView.width() / width(), false); // If FOV is set to 0 then unity FOV is assumed (faster redraws)
	generateCosineTable();
	return getFOV();
}

} // namespace Bagel
