/*****************************************************************************
 * Copyright (c) 2014-2020 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#include "LargeSceneryPlaceAction.h"

#include "../OpenRCT2.h"
#include "../management/Finance.h"
#include "../object/ObjectLimits.h"
#include "../ride/Ride.h"
#include "../world/Banner.h"
#include "../world/ConstructionClearance.h"
#include "../world/MapAnimation.h"
#include "../world/Surface.h"

LargeSceneryPlaceAction::LargeSceneryPlaceAction(
    const CoordsXYZD& loc, ObjectEntryIndex sceneryType, uint8_t primaryColour, uint8_t secondaryColour)
    : _loc(loc)
    , _sceneryType(sceneryType)
    , _primaryColour(primaryColour)
    , _secondaryColour(secondaryColour)
{
}

void LargeSceneryPlaceAction::AcceptParameters(GameActionParameterVisitor& visitor)
{
    visitor.Visit(_loc);
    visitor.Visit("object", _sceneryType);
    visitor.Visit("primaryColour", _primaryColour);
    visitor.Visit("secondaryColour", _secondaryColour);
}

uint16_t LargeSceneryPlaceAction::GetActionFlags() const
{
    return GameAction::GetActionFlags();
}

void LargeSceneryPlaceAction::Serialise(DataSerialiser& stream)
{
    GameAction::Serialise(stream);

    stream << DS_TAG(_loc) << DS_TAG(_sceneryType) << DS_TAG(_primaryColour) << DS_TAG(_secondaryColour);
}

GameActions::Result::Ptr LargeSceneryPlaceAction::Query() const
{
    auto res = MakeResult();
    res->ErrorTitle = STR_CANT_POSITION_THIS_HERE;
    res->Expenditure = ExpenditureType::Landscaping;
    int16_t surfaceHeight = tile_element_height(_loc);
    res->Position.x = _loc.x + 16;
    res->Position.y = _loc.y + 16;
    res->Position.z = surfaceHeight;

    auto resultData = LargeSceneryPlaceActionResult{};

    money32 supportsCost = 0;

    if (_primaryColour > TILE_ELEMENT_COLOUR_MASK || _secondaryColour > TILE_ELEMENT_COLOUR_MASK)
    {
        log_error(
            "Invalid game command for scenery placement, primaryColour = %u, secondaryColour = %u", _primaryColour,
            _secondaryColour);
        return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_NONE);
    }

    if (_sceneryType >= MAX_LARGE_SCENERY_OBJECTS)
    {
        log_error("Invalid game command for scenery placement, sceneryType = %u", _sceneryType);
        return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_NONE);
    }

    auto* sceneryEntry = get_large_scenery_entry(_sceneryType);
    if (sceneryEntry == nullptr)
    {
        log_error("Invalid game command for scenery placement, sceneryType = %u", _sceneryType);
        return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_NONE);
    }

    uint32_t totalNumTiles = GetTotalNumTiles(sceneryEntry->tiles);
    int16_t maxHeight = GetMaxSurfaceHeight(sceneryEntry->tiles);

    if (_loc.z != 0)
    {
        maxHeight = _loc.z;
    }

    res->Position.z = maxHeight;

    if (sceneryEntry->scrolling_mode != SCROLLING_MODE_NONE)
    {
        if (HasReachedBannerLimit())
        {
            log_error("No free banners available");
            return MakeResult(
                GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_TOO_MANY_BANNERS_IN_GAME);
        }
    }

    uint8_t tileNum = 0;
    for (rct_large_scenery_tile* tile = sceneryEntry->tiles; tile->x_offset != -1; tile++, tileNum++)
    {
        auto curTile = CoordsXY{ tile->x_offset, tile->y_offset }.Rotate(_loc.direction);

        curTile.x += _loc.x;
        curTile.y += _loc.y;

        int32_t zLow = tile->z_offset + maxHeight;
        int32_t zHigh = tile->z_clearance + zLow;

        QuarterTile quarterTile = QuarterTile{ static_cast<uint8_t>(tile->flags >> 12), 0 }.Rotate(_loc.direction);
        const auto isTree = (sceneryEntry->flags & LARGE_SCENERY_FLAG_IS_TREE) != 0;
        auto canBuild = MapCanConstructWithClearAt(
            { curTile, zLow, zHigh }, &map_place_scenery_clear_func, quarterTile, GetFlags(), CREATE_CROSSING_MODE_NONE,
            isTree);
        if (canBuild->Error != GameActions::Status::Ok)
        {
            canBuild->ErrorTitle = STR_CANT_POSITION_THIS_HERE;
            return canBuild;
        }

        supportsCost += canBuild->Cost;

        const auto clearanceData = canBuild->GetData<ConstructClearResult>();
        int32_t tempSceneryGroundFlags = clearanceData.GroundFlags & (ELEMENT_IS_ABOVE_GROUND | ELEMENT_IS_UNDERGROUND);
        if (!gCheatsDisableClearanceChecks)
        {
            if ((clearanceData.GroundFlags & ELEMENT_IS_UNDERWATER) || (clearanceData.GroundFlags & ELEMENT_IS_UNDERGROUND))
            {
                return MakeResult(GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_CANT_BUILD_THIS_UNDERWATER);
            }
            if (resultData.GroundFlags && !(resultData.GroundFlags & tempSceneryGroundFlags))
            {
                return MakeResult(
                    GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE,
                    STR_CANT_BUILD_PARTLY_ABOVE_AND_PARTLY_BELOW_GROUND);
            }
        }

        resultData.GroundFlags = tempSceneryGroundFlags;

        if (!LocationValid(curTile) || map_is_edge(curTile))
        {
            return MakeResult(GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_OFF_EDGE_OF_MAP);
        }

        if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !map_is_location_owned({ curTile, zLow }) && !gCheatsSandboxMode)
        {
            return MakeResult(GameActions::Status::Disallowed, STR_CANT_POSITION_THIS_HERE, STR_LAND_NOT_OWNED_BY_PARK);
        }
    }

    if (!CheckMapCapacity(sceneryEntry->tiles, totalNumTiles))
    {
        log_error("No free map elements available");
        return MakeResult(GameActions::Status::NoFreeElements, STR_CANT_POSITION_THIS_HERE, STR_TILE_ELEMENT_LIMIT_REACHED);
    }

    // Force ride construction to recheck area
    _currentTrackSelectionFlags |= TRACK_SELECTION_FLAG_RECHECK;

    res->Cost = (sceneryEntry->price * 10) + supportsCost;
    res->SetData(std::move(resultData));

    return res;
}

GameActions::Result::Ptr LargeSceneryPlaceAction::Execute() const
{
    auto res = MakeResult();
    res->ErrorTitle = STR_CANT_POSITION_THIS_HERE;
    res->Expenditure = ExpenditureType::Landscaping;

    int16_t surfaceHeight = tile_element_height(_loc);
    res->Position.x = _loc.x + 16;
    res->Position.y = _loc.y + 16;
    res->Position.z = surfaceHeight;

    auto resultData = LargeSceneryPlaceActionResult{};

    money32 supportsCost = 0;

    auto* sceneryEntry = get_large_scenery_entry(_sceneryType);
    if (sceneryEntry == nullptr)
    {
        log_error("Invalid game command for scenery placement, sceneryType = %u", _sceneryType);
        return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_NONE);
    }

    if (sceneryEntry->tiles == nullptr)
    {
        log_error("Invalid large scenery object, sceneryType = %u", _sceneryType);
        return MakeResult(GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_NONE);
    }

    int16_t maxHeight = GetMaxSurfaceHeight(sceneryEntry->tiles);

    if (_loc.z != 0)
    {
        maxHeight = _loc.z;
    }

    res->Position.z = maxHeight;

    // Allocate banner
    Banner* banner = nullptr;
    if (sceneryEntry->scrolling_mode != SCROLLING_MODE_NONE)
    {
        banner = CreateBanner();
        if (banner == nullptr)
        {
            log_error("No free banners available");
            return MakeResult(
                GameActions::Status::InvalidParameters, STR_CANT_POSITION_THIS_HERE, STR_TOO_MANY_BANNERS_IN_GAME);
        }

        banner->text = {};
        banner->colour = 2;
        banner->text_colour = 2;
        banner->flags = BANNER_FLAG_IS_LARGE_SCENERY;
        banner->type = 0;
        banner->position = TileCoordsXY(_loc);

        ride_id_t rideIndex = banner_get_closest_ride_index({ _loc, maxHeight });
        if (rideIndex != RIDE_ID_NULL)
        {
            banner->ride_index = rideIndex;
            banner->flags |= BANNER_FLAG_LINKED_TO_RIDE;
        }

        resultData.bannerId = banner->id;
    }

    uint8_t tileNum = 0;
    for (rct_large_scenery_tile* tile = sceneryEntry->tiles; tile->x_offset != -1; tile++, tileNum++)
    {
        auto curTile = CoordsXY{ tile->x_offset, tile->y_offset }.Rotate(_loc.direction);

        curTile.x += _loc.x;
        curTile.y += _loc.y;

        int32_t zLow = tile->z_offset + maxHeight;
        int32_t zHigh = tile->z_clearance + zLow;

        QuarterTile quarterTile = QuarterTile{ static_cast<uint8_t>(tile->flags >> 12), 0 }.Rotate(_loc.direction);
        const auto isTree = (sceneryEntry->flags & LARGE_SCENERY_FLAG_IS_TREE) != 0;
        auto canBuild = MapCanConstructWithClearAt(
            { curTile, zLow, zHigh }, &map_place_scenery_clear_func, quarterTile, GetFlags(), CREATE_CROSSING_MODE_NONE,
            isTree);
        if (canBuild->Error != GameActions::Status::Ok)
        {
            if (banner != nullptr)
            {
                DeleteBanner(banner->id);
            }
            canBuild->ErrorTitle = STR_CANT_POSITION_THIS_HERE;
            return canBuild;
        }

        supportsCost += canBuild->Cost;

        const auto clearanceData = canBuild->GetData<ConstructClearResult>();
        resultData.GroundFlags = clearanceData.GroundFlags & (ELEMENT_IS_ABOVE_GROUND | ELEMENT_IS_UNDERGROUND);

        if (!(GetFlags() & GAME_COMMAND_FLAG_GHOST))
        {
            footpath_remove_litter({ curTile, zLow });
            if (!gCheatsDisableClearanceChecks)
            {
                wall_remove_at({ curTile, zLow, zHigh });
            }
        }

        auto* newSceneryElement = TileElementInsert<LargeSceneryElement>(
            CoordsXYZ{ curTile.x, curTile.y, zLow }, quarterTile.GetBaseQuarterOccupied());
        Guard::Assert(newSceneryElement != nullptr);
        newSceneryElement->SetClearanceZ(zHigh);

        SetNewLargeSceneryElement(*newSceneryElement, tileNum);
        if (banner != nullptr)
        {
            newSceneryElement->SetBannerIndex(banner->id);
        }

        map_animation_create(MAP_ANIMATION_TYPE_LARGE_SCENERY, { curTile, zLow });
        map_invalidate_tile_full(curTile);

        if (tileNum == 0)
        {
            resultData.firstTileHeight = zLow;
        }
    }

    // Force ride construction to recheck area
    _currentTrackSelectionFlags |= TRACK_SELECTION_FLAG_RECHECK;

    res->Cost = (sceneryEntry->price * 10) + supportsCost;
    res->SetData(std::move(resultData));

    return res;
}

int16_t LargeSceneryPlaceAction::GetTotalNumTiles(rct_large_scenery_tile* tiles) const
{
    uint32_t totalNumTiles = 0;
    for (rct_large_scenery_tile* tile = tiles; tile->x_offset != -1; tile++)
    {
        totalNumTiles++;
    }
    return totalNumTiles;
}

bool LargeSceneryPlaceAction::CheckMapCapacity(rct_large_scenery_tile* tiles, int16_t numTiles) const
{
    for (rct_large_scenery_tile* tile = tiles; tile->x_offset != -1; tile++)
    {
        auto curTile = CoordsXY{ tile->x_offset, tile->y_offset }.Rotate(_loc.direction);

        curTile.x += _loc.x;
        curTile.y += _loc.y;
        if (!MapCheckCapacityAndReorganise(curTile, numTiles))
        {
            return false;
        }
    }
    return true;
}

int16_t LargeSceneryPlaceAction::GetMaxSurfaceHeight(rct_large_scenery_tile* tiles) const
{
    int16_t maxHeight = -1;
    for (rct_large_scenery_tile* tile = tiles; tile->x_offset != -1; tile++)
    {
        auto curTile = CoordsXY{ tile->x_offset, tile->y_offset }.Rotate(_loc.direction);

        curTile.x += _loc.x;
        curTile.y += _loc.y;

        if (!map_is_location_valid(curTile))
        {
            continue;
        }

        auto* surfaceElement = map_get_surface_element_at(curTile);
        if (surfaceElement == nullptr)
            continue;

        int32_t baseZ = surfaceElement->GetBaseZ();
        int32_t slope = surfaceElement->GetSlope();

        if ((slope & TILE_ELEMENT_SLOPE_ALL_CORNERS_UP) != TILE_ELEMENT_SLOPE_FLAT)
        {
            baseZ += LAND_HEIGHT_STEP;
            if (slope & TILE_ELEMENT_SLOPE_DOUBLE_HEIGHT)
            {
                baseZ += LAND_HEIGHT_STEP;
            }
        }

        if (baseZ > maxHeight)
        {
            maxHeight = baseZ;
        }
    }
    return maxHeight;
}

void LargeSceneryPlaceAction::SetNewLargeSceneryElement(LargeSceneryElement& sceneryElement, uint8_t tileNum) const
{
    sceneryElement.SetDirection(_loc.direction);
    sceneryElement.SetEntryIndex(_sceneryType);
    sceneryElement.SetSequenceIndex(tileNum);
    sceneryElement.SetPrimaryColour(_primaryColour);
    sceneryElement.SetSecondaryColour(_secondaryColour);

    if (GetFlags() & GAME_COMMAND_FLAG_GHOST)
    {
        sceneryElement.SetGhost(true);
    }
}
