/*
 * This file is part of OpenTTD.
 * OpenTTD 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, version 2.
 * OpenTTD 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 OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
 */

/** @file effectvehicle.cpp Implementation of everything generic to vehicles. */

#include "stdafx.h"
#include "landscape.h"
#include "core/random_func.hpp"
#include "industry_map.h"
#include "vehicle_func.h"
#include "sound_func.h"
#include "animated_tile_func.h"
#include "effectvehicle_func.h"
#include "effectvehicle_base.h"

#include "safeguards.h"


/**
 * Increment the sprite unless it has reached the end of the animation.
 * @param v Vehicle to increment sprite of.
 * @param last Last sprite of animation.
 * @return true if the sprite was incremented, false if the end was reached.
 */
static bool IncrementSprite(EffectVehicle *v, SpriteID last)
{
	if (v->sprite_cache.sprite_seq.seq[0].sprite != last) {
		v->sprite_cache.sprite_seq.seq[0].sprite++;
		return true;
	} else {
		return false;
	}
}

static void ChimneySmokeInit(EffectVehicle *v)
{
	uint32_t r = Random();
	v->sprite_cache.sprite_seq.Set(SPR_CHIMNEY_SMOKE_0 + GB(r, 0, 3));
	v->progress = GB(r, 16, 3);
}

static bool ChimneySmokeTick(EffectVehicle *v)
{
	if (v->progress > 0) {
		v->progress--;
	} else {
		TileIndex tile = TileVirtXY(v->x_pos, v->y_pos);
		if (!IsTileType(tile, MP_INDUSTRY)) {
			delete v;
			return false;
		}

		if (!IncrementSprite(v, SPR_CHIMNEY_SMOKE_7)) {
			v->sprite_cache.sprite_seq.Set(SPR_CHIMNEY_SMOKE_0);
		}
		v->progress = 7;
		v->UpdatePositionAndViewport();
	}

	return true;
}

static void SteamSmokeInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_STEAM_SMOKE_0);
	v->progress = 12;
}

static bool SteamSmokeTick(EffectVehicle *v)
{
	bool moved = false;

	v->progress++;

	if ((v->progress & 7) == 0) {
		v->z_pos++;
		moved = true;
	}

	if ((v->progress & 0xF) == 4) {
		if (!IncrementSprite(v, SPR_STEAM_SMOKE_4)) {
			delete v;
			return false;
		}
		moved = true;
	}

	if (moved) v->UpdatePositionAndViewport();

	return true;
}

static void DieselSmokeInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_DIESEL_SMOKE_0);
	v->progress = 0;
}

static bool DieselSmokeTick(EffectVehicle *v)
{
	v->progress++;

	if ((v->progress & 3) == 0) {
		v->z_pos++;
		v->UpdatePositionAndViewport();
	} else if ((v->progress & 7) == 1) {
		if (!IncrementSprite(v, SPR_DIESEL_SMOKE_5)) {
			delete v;
			return false;
		}
		v->UpdatePositionAndViewport();
	}

	return true;
}

static void ElectricSparkInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_ELECTRIC_SPARK_0);
	v->progress = 1;
}

static bool ElectricSparkTick(EffectVehicle *v)
{
	if (v->progress < 2) {
		v->progress++;
	} else {
		v->progress = 0;

		if (!IncrementSprite(v, SPR_ELECTRIC_SPARK_5)) {
			delete v;
			return false;
		}
		v->UpdatePositionAndViewport();
	}

	return true;
}

static void SmokeInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_SMOKE_0);
	v->progress = 12;
}

static bool SmokeTick(EffectVehicle *v)
{
	bool moved = false;

	v->progress++;

	if ((v->progress & 3) == 0) {
		v->z_pos++;
		moved = true;
	}

	if ((v->progress & 0xF) == 4) {
		if (!IncrementSprite(v, SPR_SMOKE_4)) {
			delete v;
			return false;
		}
		moved = true;
	}

	if (moved) v->UpdatePositionAndViewport();

	return true;
}

static void ExplosionLargeInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_EXPLOSION_LARGE_0);
	v->progress = 0;
}

static bool ExplosionLargeTick(EffectVehicle *v)
{
	v->progress++;
	if ((v->progress & 3) == 0) {
		if (!IncrementSprite(v, SPR_EXPLOSION_LARGE_F)) {
			delete v;
			return false;
		}
		v->UpdatePositionAndViewport();
	}

	return true;
}

static void BreakdownSmokeInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_BREAKDOWN_SMOKE_0);
	v->progress = 0;
}

static bool BreakdownSmokeTick(EffectVehicle *v)
{
	v->progress++;
	if ((v->progress & 7) == 0) {
		if (!IncrementSprite(v, SPR_BREAKDOWN_SMOKE_3)) {
			v->sprite_cache.sprite_seq.Set(SPR_BREAKDOWN_SMOKE_0);
		}
		v->UpdatePositionAndViewport();
	}

	v->animation_state--;
	if (v->animation_state == 0) {
		delete v;
		return false;
	}

	return true;
}

static void ExplosionSmallInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_EXPLOSION_SMALL_0);
	v->progress = 0;
}

static bool ExplosionSmallTick(EffectVehicle *v)
{
	v->progress++;
	if ((v->progress & 3) == 0) {
		if (!IncrementSprite(v, SPR_EXPLOSION_SMALL_B)) {
			delete v;
			return false;
		}
		v->UpdatePositionAndViewport();
	}

	return true;
}

static void BulldozerInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_BULLDOZER_NE);
	v->progress = 0;
	v->animation_state = 0;
	v->animation_substate = 0;
}

struct BulldozerMovement {
	uint8_t direction:2;
	uint8_t image:2;
	uint8_t duration:3;
};

static const BulldozerMovement _bulldozer_movement[] = {
	{ 0, 0, 4 },
	{ 3, 3, 4 },
	{ 2, 2, 7 },
	{ 0, 2, 7 },
	{ 1, 1, 3 },
	{ 2, 2, 7 },
	{ 0, 2, 7 },
	{ 1, 1, 3 },
	{ 2, 2, 7 },
	{ 0, 2, 7 },
	{ 3, 3, 6 },
	{ 2, 2, 6 },
	{ 1, 1, 7 },
	{ 3, 1, 7 },
	{ 0, 0, 3 },
	{ 1, 1, 7 },
	{ 3, 1, 7 },
	{ 0, 0, 3 },
	{ 1, 1, 7 },
	{ 3, 1, 7 }
};

static const struct {
	int8_t x;
	int8_t y;
} _inc_by_dir[] = {
	{ -1,  0 },
	{  0,  1 },
	{  1,  0 },
	{  0, -1 }
};

static bool BulldozerTick(EffectVehicle *v)
{
	v->progress++;
	if ((v->progress & 7) == 0) {
		const BulldozerMovement *b = &_bulldozer_movement[v->animation_state];

		v->sprite_cache.sprite_seq.Set(SPR_BULLDOZER_NE + b->image);

		v->x_pos += _inc_by_dir[b->direction].x;
		v->y_pos += _inc_by_dir[b->direction].y;

		v->animation_substate++;
		if (v->animation_substate >= b->duration) {
			v->animation_substate = 0;
			v->animation_state++;
			if (v->animation_state == lengthof(_bulldozer_movement)) {
				delete v;
				return false;
			}
		}
		v->UpdatePositionAndViewport();
	}

	return true;
}

static void BubbleInit(EffectVehicle *v)
{
	v->sprite_cache.sprite_seq.Set(SPR_BUBBLE_GENERATE_0);
	v->spritenum = 0;
	v->progress = 0;
}

struct BubbleMovement {
	int8_t x:4;
	int8_t y:4;
	int8_t z:4;
	uint8_t image:4;
};

#define MK(x, y, z, i) { x, y, z, i }
#define ME(i) { i, 4, 0, 0 }

static const BubbleMovement _bubble_float_sw[] = {
	MK(0, 0, 1, 0),
	MK(1, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(1, 0, 1, 2),
	ME(1)
};


static const BubbleMovement _bubble_float_ne[] = {
	MK( 0, 0, 1, 0),
	MK(-1, 0, 1, 1),
	MK( 0, 0, 1, 0),
	MK(-1, 0, 1, 2),
	ME(1)
};

static const BubbleMovement _bubble_float_se[] = {
	MK(0, 0, 1, 0),
	MK(0, 1, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 1, 1, 2),
	ME(1)
};

static const BubbleMovement _bubble_float_nw[] = {
	MK(0,  0, 1, 0),
	MK(0, -1, 1, 1),
	MK(0,  0, 1, 0),
	MK(0, -1, 1, 2),
	ME(1)
};

static const BubbleMovement _bubble_burst[] = {
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 7),
	MK(0, 0, 1, 8),
	MK(0, 0, 1, 9),
	ME(0)
};

static const BubbleMovement _bubble_absorb[] = {
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(0, 0, 1, 1),
	MK(2, 1, 3, 0),
	MK(1, 1, 3, 1),
	MK(2, 1, 3, 0),
	MK(1, 1, 3, 2),
	MK(2, 1, 3, 0),
	MK(1, 1, 3, 1),
	MK(2, 1, 3, 0),
	MK(1, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(1, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(1, 0, 1, 2),
	MK(0, 0, 1, 0),
	MK(1, 0, 1, 1),
	MK(0, 0, 1, 0),
	MK(1, 0, 1, 2),
	ME(2),
	MK(0, 0, 0, 0xA),
	MK(0, 0, 0, 0xB),
	MK(0, 0, 0, 0xC),
	MK(0, 0, 0, 0xD),
	MK(0, 0, 0, 0xE),
	ME(0)
};
#undef ME
#undef MK

static const BubbleMovement * const _bubble_movement[] = {
	_bubble_float_sw,
	_bubble_float_ne,
	_bubble_float_se,
	_bubble_float_nw,
	_bubble_burst,
	_bubble_absorb,
};

static bool BubbleTick(EffectVehicle *v)
{
	uint anim_state;

	v->progress++;
	if ((v->progress & 3) != 0) return true;

	if (v->spritenum == 0) {
		v->sprite_cache.sprite_seq.seq[0].sprite++;
		if (v->sprite_cache.sprite_seq.seq[0].sprite < SPR_BUBBLE_GENERATE_3) {
			v->UpdatePositionAndViewport();
			return true;
		}
		if (v->animation_substate != 0) {
			v->spritenum = GB(Random(), 0, 2) + 1;
		} else {
			v->spritenum = 6;
		}
		anim_state = 0;
	} else {
		anim_state = v->animation_state + 1;
	}

	const BubbleMovement *b = &_bubble_movement[v->spritenum - 1][anim_state];

	if (b->y == 4 && b->x == 0) {
		delete v;
		return false;
	}

	if (b->y == 4 && b->x == 1) {
		if (v->z_pos > 180 || Chance16I(1, 96, Random())) {
			v->spritenum = 5;
			if (_settings_client.sound.ambient) SndPlayVehicleFx(SND_2F_BUBBLE_GENERATOR_FAIL, v);
		}
		anim_state = 0;
	}

	if (b->y == 4 && b->x == 2) {
		TileIndex tile;

		anim_state++;
		if (_settings_client.sound.ambient) SndPlayVehicleFx(SND_31_BUBBLE_GENERATOR_SUCCESS, v);

		tile = TileVirtXY(v->x_pos, v->y_pos);
		if (IsTileType(tile, MP_INDUSTRY) && GetIndustryGfx(tile) == GFX_BUBBLE_CATCHER) AddAnimatedTile(tile);
	}

	v->animation_state = anim_state;
	b = &_bubble_movement[v->spritenum - 1][anim_state];

	v->x_pos += b->x;
	v->y_pos += b->y;
	v->z_pos += b->z;
	v->sprite_cache.sprite_seq.Set(SPR_BUBBLE_0 + b->image);

	v->UpdatePositionAndViewport();

	return true;
}

struct EffectProcs {
	using InitProc = void(EffectVehicle *);
	using TickProc = bool(EffectVehicle *);

	InitProc *init_proc; ///< Function to initialise an effect vehicle after construction.
	TickProc *tick_proc; ///< Functions for controlling effect vehicles at each tick.
	TransparencyOption transparency; ///< Transparency option affecting the effect.

	constexpr EffectProcs(InitProc *init_proc, TickProc *tick_proc, TransparencyOption transparency)
		: init_proc(init_proc), tick_proc(tick_proc), transparency(transparency) {}
};

/** Per-EffectVehicleType handling. */
static const std::array<EffectProcs, EV_END> _effect_procs = {{
	{ ChimneySmokeInit,   ChimneySmokeTick,   TO_INDUSTRIES }, // EV_CHIMNEY_SMOKE
	{ SteamSmokeInit,     SteamSmokeTick,     TO_INVALID    }, // EV_STEAM_SMOKE
	{ DieselSmokeInit,    DieselSmokeTick,    TO_INVALID    }, // EV_DIESEL_SMOKE
	{ ElectricSparkInit,  ElectricSparkTick,  TO_INVALID    }, // EV_ELECTRIC_SPARK
	{ SmokeInit,          SmokeTick,          TO_INVALID    }, // EV_CRASH_SMOKE
	{ ExplosionLargeInit, ExplosionLargeTick, TO_INVALID    }, // EV_EXPLOSION_LARGE
	{ BreakdownSmokeInit, BreakdownSmokeTick, TO_INVALID    }, // EV_BREAKDOWN_SMOKE
	{ ExplosionSmallInit, ExplosionSmallTick, TO_INVALID    }, // EV_EXPLOSION_SMALL
	{ BulldozerInit,      BulldozerTick,      TO_INVALID    }, // EV_BULLDOZER
	{ BubbleInit,         BubbleTick,         TO_INDUSTRIES }, // EV_BUBBLE
	{ SmokeInit,          SmokeTick,          TO_INVALID    }, // EV_BREAKDOWN_SMOKE_AIRCRAFT
	{ SmokeInit,          SmokeTick,          TO_INDUSTRIES }, // EV_COPPER_MINE_SMOKE
}};

/**
 * Create an effect vehicle at a particular location.
 * @param x The x location on the map.
 * @param y The y location on the map.
 * @param z The z location on the map.
 * @param type The type of effect vehicle.
 * @return The effect vehicle.
 */
EffectVehicle *CreateEffectVehicle(int x, int y, int z, EffectVehicleType type)
{
	if (!Vehicle::CanAllocateItem()) return nullptr;

	EffectVehicle *v = new EffectVehicle();
	v->subtype = type;
	v->x_pos = x;
	v->y_pos = y;
	v->z_pos = z;
	v->tile = TileIndex{};
	v->UpdateDeltaXY();
	v->vehstatus = VehState::Unclickable;

	_effect_procs[type].init_proc(v);

	v->UpdatePositionAndViewport();

	return v;
}

/**
 * Create an effect vehicle above a particular location.
 * @param x The x location on the map.
 * @param y The y location on the map.
 * @param z The offset from the ground.
 * @param type The type of effect vehicle.
 * @return The effect vehicle.
 */
EffectVehicle *CreateEffectVehicleAbove(int x, int y, int z, EffectVehicleType type)
{
	int safe_x = Clamp(x, 0, Map::MaxX() * TILE_SIZE);
	int safe_y = Clamp(y, 0, Map::MaxY() * TILE_SIZE);
	return CreateEffectVehicle(x, y, GetSlopePixelZ(safe_x, safe_y) + z, type);
}

/**
 * Create an effect vehicle above a particular vehicle.
 * @param v The vehicle to base the position on.
 * @param x The x offset to the vehicle.
 * @param y The y offset to the vehicle.
 * @param z The z offset to the vehicle.
 * @param type The type of effect vehicle.
 * @return The effect vehicle.
 */
EffectVehicle *CreateEffectVehicleRel(const Vehicle *v, int x, int y, int z, EffectVehicleType type)
{
	return CreateEffectVehicle(v->x_pos + x, v->y_pos + y, v->z_pos + z, type);
}

bool EffectVehicle::Tick()
{
	return _effect_procs[this->subtype].tick_proc(this);
}

void EffectVehicle::UpdateDeltaXY()
{
	this->bounds = {{}, {1, 1, 1}, {}};
}

/**
 * Determines the transparency option affecting the effect.
 * @return Transparency option, or TO_INVALID if none.
 */
TransparencyOption EffectVehicle::GetTransparencyOption() const
{
	return _effect_procs[this->subtype].transparency;
}
