/* 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.
 *
 * Additional copyright for this file:
 * Copyright (C) 1995-1997 Presto Studios, Inc.
 *
 * 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 "common/macresman.h"
#include "common/stream.h"

#include "pegasus/elements.h"
#include "pegasus/graphics.h"
#include "pegasus/surface.h"

namespace Pegasus {

DisplayElement::DisplayElement(const DisplayElementID id) : IDObject(id) {
	_elementIsDisplaying = false;
	_elementIsVisible = false;
	_elementOrder = 0;
	_triggeredElement = this;
	_nextElement = nullptr;
}

DisplayElement::~DisplayElement() {
	if (isDisplaying())
		((PegasusEngine *)g_engine)->_gfx->removeDisplayElement(this);
}

void DisplayElement::setDisplayOrder(const DisplayOrder order) {
	if (_elementOrder != order) {
		_elementOrder = order;
		if (isDisplaying()) {
			((PegasusEngine *)g_engine)->_gfx->removeDisplayElement(this);
			((PegasusEngine *)g_engine)->_gfx->addDisplayElement(this);
			triggerRedraw();
		}
	}
}

void DisplayElement::startDisplaying() {
	if (!isDisplaying()) {
		((PegasusEngine *)g_engine)->_gfx->addDisplayElement(this);
		triggerRedraw();
	}
}

void DisplayElement::stopDisplaying() {
	if (isDisplaying()) {
		triggerRedraw();
		((PegasusEngine *)g_engine)->_gfx->removeDisplayElement(this);
	}
}

void DisplayElement::setBounds(const CoordType left, const CoordType top, const CoordType right, const CoordType bottom) {
	setBounds(Common::Rect(left, top, right, bottom));
}

void DisplayElement::getBounds(Common::Rect &r) const {
	r = _bounds;
}

void DisplayElement::sizeElement(const CoordType h, const CoordType v) {
	Common::Rect newBounds = _bounds;
	newBounds.right = _bounds.left + h;
	newBounds.bottom = _bounds.top + v;
	setBounds(newBounds);
}

void DisplayElement::moveElementTo(const CoordType h, const CoordType v) {
	Common::Rect newBounds = _bounds;
	newBounds.moveTo(h, v);
	setBounds(newBounds);
}

void DisplayElement::moveElement(const CoordType dh, const CoordType dv) {
	Common::Rect newBounds = _bounds;
	newBounds.translate(dh, dv);
	setBounds(newBounds);
}

void DisplayElement::getLocation(CoordType &h, CoordType &v) const {
	h = _bounds.left;
	v = _bounds.top;
}

void DisplayElement::centerElementAt(const CoordType h, const CoordType v) {
	Common::Rect newBounds = _bounds;
	newBounds.moveTo(h - (_bounds.width() / 2), v - (_bounds.height() / 2));
	setBounds(newBounds);
}

void DisplayElement::getCenter(CoordType &h, CoordType &v) const {
	h = (_bounds.left + _bounds.right) / 2;
	v = (_bounds.top + _bounds.bottom) / 2;
}

void DisplayElement::setBounds(const Common::Rect &r) {
	if (r != _bounds) {
		triggerRedraw();
		_bounds = r;
		triggerRedraw();
	}
}

void DisplayElement::hide() {
	if (_elementIsVisible) {
		triggerRedraw();
		_elementIsVisible = false;
	}
}

void DisplayElement::show() {
	if (!_elementIsVisible) {
		_elementIsVisible = true;
		triggerRedraw();
	}
}

// Only invalidates this element's bounding rectangle if all these conditions are true:
// -- The triggered element is this element.
// -- The element is displaying on the display list.
// -- The element is visible.
// -- The element is part of the active layer OR is one of the reserved items.
void DisplayElement::triggerRedraw() {
	GraphicsManager *gfx = ((PegasusEngine *)g_engine)->_gfx;

	if (_triggeredElement == this) {
		if (validToDraw(gfx->getBackOfActiveLayer(), gfx->getFrontOfActiveLayer()))
			gfx->invalRect(_bounds);
	} else {
		_triggeredElement->triggerRedraw();
	}
}

void DisplayElement::setTriggeredElement(DisplayElement *element) {
	if (element)
		_triggeredElement = element;
	else
		_triggeredElement = this;
}

bool DisplayElement::validToDraw(DisplayOrder backLayer, DisplayOrder frontLayer) {
	return	isDisplaying() && _elementIsVisible &&
			(getObjectID() <= kHighestReservedElementID ||
			(getDisplayOrder() >= backLayer &&
			getDisplayOrder() <= frontLayer));
}

DropHighlight::DropHighlight(const DisplayElementID id) : DisplayElement(id) {
	_highlightColor = 0;
	_thickness = 2;
	_cornerDiameter = 0;
}

void DropHighlight::draw(const Common::Rect &) {
	Graphics::Surface *screen = ((PegasusEngine *)g_engine)->_gfx->getWorkArea();

	// Since this is only used in two different ways, I'm only
	// going to implement it in those two ways. Deal with it.

	Common::Rect rect = _bounds;
	rect.grow(-_thickness);
	screen->frameRect(rect, _highlightColor);
	rect.grow(1);
	screen->frameRect(rect, _highlightColor);

	if (_cornerDiameter == 8 && _thickness == 4) {
		rect.grow(1);
		screen->frameRect(rect, _highlightColor);
		screen->hLine(rect.left + 1, rect.top - 1, rect.right - 2, _highlightColor);
		screen->hLine(rect.left + 1, rect.bottom, rect.right - 2, _highlightColor);
		screen->vLine(rect.left - 1, rect.top + 1, rect.bottom - 2, _highlightColor);
		screen->vLine(rect.right, rect.top + 1, rect.bottom - 2, _highlightColor);
	}
}

IdlerAnimation::IdlerAnimation(const DisplayElementID id) : Animation(id) {
	_lastTime = 0xffffffff;
}

void IdlerAnimation::startDisplaying() {
	if (!isDisplaying()) {
		Animation::startDisplaying();
		startIdling();
	}
}

void IdlerAnimation::stopDisplaying() {
	if (isDisplaying()) {
		Animation::stopDisplaying();
		stopIdling();
	}
}

void IdlerAnimation::useIdleTime() {
	uint32 currentTime = getTime();

	if (currentTime != _lastTime) {
		_lastTime = currentTime;
		timeChanged(_lastTime);
	}
}

void IdlerAnimation::timeChanged(const TimeValue) {
	triggerRedraw();
}

FrameSequence::FrameSequence(const DisplayElementID id) : IdlerAnimation(id) {
	_duration = 0;
	_currentFrameNum = 0;
	_resFork = new Common::MacResManager();
	_numFrames = 0;
}

FrameSequence::~FrameSequence() {
	delete _resFork;
}

void FrameSequence::useFileName(const Common::String &fileName) {
	_resFork->open(fileName);
}

void FrameSequence::openFrameSequence() {
	if (!_resFork->hasResFork())
		return;

	Common::SeekableReadStream *res = _resFork->getResource(MKTAG('P', 'F', 'r', 'm'), 0x80);

	if (!res)
		return;

	uint32 scale = res->readUint32BE();
	_bounds.top = res->readUint16BE();
	_bounds.left = res->readUint16BE();
	_bounds.bottom = res->readUint16BE();
	_bounds.right = res->readUint16BE();
	_numFrames = res->readUint16BE();
	_duration = 0;

	_frameTimes.clear();
	for (uint32 i = 0; i < _numFrames; i++) {
		TimeValue time = res->readUint32BE();
		_frameTimes.push_back(_duration);
		_duration += time;
	}

	setScale(scale);
	setSegment(0, _duration);
	setTime(0);
	_currentFrameNum = 0;
	newFrame(_currentFrameNum);
	triggerRedraw();

	delete res;
}

void FrameSequence::closeFrameSequence() {
	stop();
	_resFork->close();
	_duration = 0;
	_numFrames = 0;
	_frameTimes.clear();
}

void FrameSequence::timeChanged(const TimeValue time) {
	int16 frameNum = 0;
	for (int16 i = _numFrames - 1; i >= 0; i--) {
		if (_frameTimes[i] < time) {
			frameNum = i;
			break;
		}
	}

	if (frameNum != _currentFrameNum) {
		_currentFrameNum = frameNum;
		newFrame(_currentFrameNum);
		triggerRedraw();
	}
}

void FrameSequence::setFrameNum(const int16 frameNum) {
	int16 f = CLIP<int>(frameNum, 0, _numFrames);

	if (_currentFrameNum != f) {
		_currentFrameNum = f;
		setTime(_frameTimes[f]);
		newFrame(f);
		triggerRedraw();
	}
}

bool FrameSequence::isSequenceOpen() const {
	return _numFrames != 0;
}

Sprite::Sprite(const DisplayElementID id) : DisplayElement(id) {
	_numFrames = 0;
	_currentFrameNum = 0xffffffff;
	_currentFrame = nullptr;
}

Sprite::~Sprite() {
	discardFrames();
}

void Sprite::discardFrames() {
	if (!_frameArray.empty()) {
		for (uint32 i = 0; i < _numFrames; i++) {
			SpriteFrame *frame = _frameArray[i].frame;
			frame->_referenceCount--;
			if (frame->_referenceCount == 0)
				delete frame;
		}

		_frameArray.clear();
		_numFrames = 0;
		_currentFrame = nullptr;
		_currentFrameNum = 0xffffffff;
		setBounds(0, 0, 0, 0);
	}
}

void Sprite::addPICTResourceFrame(const ResIDType pictID, bool transparent, const CoordType left, const CoordType top) {
	SpriteFrame *frame = new SpriteFrame();
	frame->initFromPICTResource(((PegasusEngine *)g_engine)->_resFork, pictID, transparent);
	addFrame(frame, left, top);
}

uint32 Sprite::addFrame(SpriteFrame *frame, const CoordType left, const CoordType top) {
	SpriteFrameRec frameRecord;
	frameRecord.frame = frame;
	frameRecord.frameLeft = left;
	frameRecord.frameTop = top;
	_frameArray.push_back(frameRecord);
	_numFrames++;
	frame->_referenceCount++;

	Common::Rect frameBounds;
	frame->getSurfaceBounds(frameBounds);

	// 9/3/96
	// BB Should this be + left or - left?
	frameBounds.moveTo(_bounds.left + left, _bounds.top + top);

	frameBounds.extend(_bounds);

	if (_bounds != frameBounds)
		setBounds(frameBounds);

	return _numFrames - 1;
}

void Sprite::removeFrame(const uint32 frameNum) {
	_frameArray[frameNum].frame->_referenceCount--;
	if (_frameArray[frameNum].frame->_referenceCount == 0)
		delete _frameArray[frameNum].frame;

	// Calculate the new bounds
	Common::Rect frameBounds;
	for (uint32 i = 0; i < _numFrames; i++) {
		if (i == frameNum)
			continue;

		Common::Rect r;
		_frameArray[i].frame->getSurfaceBounds(r);
		r.translate(_frameArray[i].frameLeft, _frameArray[i].frameTop);
		frameBounds.extend(r);
	}

	_frameArray.remove_at(frameNum);

	frameBounds.moveTo(_bounds.left, _bounds.top);
	setBounds(frameBounds);

	if (_currentFrameNum == frameNum)
		triggerRedraw();
	else if (_currentFrameNum != 0xffffffff && _currentFrameNum > frameNum)
		--_currentFrameNum;
}

void Sprite::setCurrentFrameIndex(const int32 frameNum) {
	if (frameNum < 0) {
		if (_currentFrameNum != 0xffffffff) {
			_currentFrameNum = 0xffffffff;
			_currentFrame = nullptr;
			triggerRedraw();
		}
	} else if (_numFrames > 0) {
		uint32 f = frameNum % _numFrames;
		if (f != _currentFrameNum) {
			_currentFrameNum = f;
			_currentFrame = &_frameArray[f];
			triggerRedraw();
		}
	}
}

SpriteFrame *Sprite::getFrame(const int32 index) {
	if (index < 0 || (uint32)index >= _numFrames)
		return nullptr;

	return _frameArray[index].frame;
}

void Sprite::draw(const Common::Rect &r) {
	if (_currentFrame) {
		Common::Rect frameBounds;
		_currentFrame->frame->getSurfaceBounds(frameBounds);

		frameBounds.translate(_bounds.left + _currentFrame->frameLeft, _bounds.top + _currentFrame->frameTop);
		Common::Rect r1 = frameBounds.findIntersectingRect(r);

		Common::Rect r2 = frameBounds;
		r2.translate(-_bounds.left - _currentFrame->frameLeft, -_bounds.top - _currentFrame->frameTop);

		_currentFrame->frame->drawImage(r2, r1);
	}
}

SpriteSequence::SpriteSequence(const DisplayElementID id, const DisplayElementID spriteID) :
		FrameSequence(id), _sprite(spriteID), _transparent(false) {
}

void SpriteSequence::openFrameSequence() {
	if (!isSequenceOpen()) {
		FrameSequence::openFrameSequence();

		if (isSequenceOpen()) {
			uint32 numFrames = getNumFrames();

			for (uint32 i = 0; i < numFrames; ++i) {
				SpriteFrame *frame = new SpriteFrame();
				frame->initFromPICTResource(_resFork, i + 0x80, _transparent);
				_sprite.addFrame(frame, 0, 0);
			}

			_sprite.setBounds(_bounds);
		}
	}
}

void SpriteSequence::closeFrameSequence() {
	if (isSequenceOpen()) {
		FrameSequence::closeFrameSequence();
		_sprite.discardFrames();
	}
}

void SpriteSequence::setBounds(const Common::Rect &bounds) {
	FrameSequence::setBounds(bounds);
	_sprite.setBounds(_bounds);
}

void SpriteSequence::draw(const Common::Rect &r) {
	_sprite.draw(r);
}

void SpriteSequence::newFrame(const uint16 frame) {
	_sprite.setCurrentFrameIndex(frame);
}

#define DRAW_PIXEL() \
	if (bytesPerPixel == 2) \
		*((uint16 *)dst) = black; \
	else \
		*((uint32 *)dst) = black; \
	dst += bytesPerPixel

#define SKIP_PIXEL() \
	dst += bytesPerPixel

void ScreenDimmer::draw(const Common::Rect &r) {
	// We're going to emulate QuickDraw's srcOr+gray mode here
	// In this mode, every other y column is all black (odd-columns).
	// Basically, every row does three black and then one transparent
	// repeatedly.

	// The output is identical to the original

	uint32 black = g_system->getScreenFormat().RGBToColor(0, 0, 0);
	Graphics::Surface *screen = ((PegasusEngine *)g_engine)->_gfx->getWorkArea();
	byte bytesPerPixel = g_system->getScreenFormat().bytesPerPixel;

	// We're currently doing it to the whole screen to simplify the code

	for (int y = 0; y < 480; y++) {
		byte *dst = (byte *)screen->getBasePtr(0, y);

		for (int x = 0; x < 640; x += 4) {
			if (y & 1) {
				DRAW_PIXEL();
				DRAW_PIXEL();
				SKIP_PIXEL();
				DRAW_PIXEL();
			} else {
				SKIP_PIXEL();
				DRAW_PIXEL();
				DRAW_PIXEL();
				DRAW_PIXEL();
			}
		}
	}
}

#undef DRAW_PIXEL
#undef SKIP_PIXEL

SoundLevel::SoundLevel(const DisplayElementID id) : DisplayElement(id) {
	_soundLevel = 0;
}

void SoundLevel::incrementLevel() {
	if (_soundLevel < 12) {
		_soundLevel++;
		triggerRedraw();
	}
}

void SoundLevel::decrementLevel() {
	if (_soundLevel > 0) {
		_soundLevel--;
		triggerRedraw();
	}
}

uint16 SoundLevel::getSoundLevel() {
	return CLIP<int>(_soundLevel * 22, 0, 256);
}

void SoundLevel::setSoundLevel(uint16 level) {
	uint16 newLevel = (level + 21) / 22;

	if (newLevel != _soundLevel) {
		_soundLevel = newLevel;
		triggerRedraw();
	}
}

void SoundLevel::draw(const Common::Rect &r) {
	Common::Rect levelRect(_bounds.right + (8 * (_soundLevel - 12)), _bounds.top, _bounds.right, _bounds.bottom);
	levelRect = r.findIntersectingRect(levelRect);

	if (!levelRect.isEmpty()) {
		Graphics::Surface *screen = ((PegasusEngine *)g_engine)->_gfx->getWorkArea();
		screen->fillRect(levelRect, g_system->getScreenFormat().RGBToColor(0, 0, 0));
	}
}

} // End of namespace Pegasus
