/*
 * This file is part of the Colobot: Gold Edition source code
 * Copyright (C) 2001-2023, Daniel Roux, EPSITEC SA & TerranovaTeam
 * http://epsitec.ch; http://colobot.info; http://github.com/colobot
 *
 * 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://gnu.org/licenses
 */


#include "graphics/engine/terrain.h"

#include "app/app.h"

#include "common/image.h"
#include "common/logger.h"

#include "graphics/engine/engine.h"
#include "graphics/engine/water.h"

#include "math/geometry.h"

#include <sstream>

#include <SDL.h>


// Graphics module namespace
namespace Gfx
{


CTerrain::CTerrain()
{
    m_engine = CEngine::GetInstancePointer();
    m_water  = m_engine->GetWater();

    m_mosaicCount     = 20;
    m_brickCount      = 1 << 4;
    m_brickSize       = 10.0f;
    m_vision          = 200.0f;
    m_textureScale    = 0.01f;
    m_scaleRelief     = 1.0f;
    m_textureSubdivCount   = 1;
    m_depth           = 2;
    m_maxMaterialID   = 0;
    m_wind            = Math::Vector(0.0f, 0.0f, 0.0f);
    m_defaultHardness = 0.5f;
    m_useMaterials    = false;

    m_flyingMaxHeight = 0.0f;
    m_maxMaterialID = 0;
    m_materialAutoID = 0;
    m_materialPointCount = 0;

    FlushBuildingLevel();
    FlushFlyingLimit();
    FlushMaterials();
}

CTerrain::~CTerrain()
{
}

bool CTerrain::Generate(int mosaicCount, int brickCountPow2, float brickSize,
                        float vision, int depth, float hardness)
{
    m_mosaicCount   = mosaicCount;
    m_brickCount    = 1 << brickCountPow2;
    m_brickSize     = brickSize;
    m_vision        = vision;
    m_depth         = depth;
    m_defaultHardness   = hardness;

    m_engine->SetTerrainVision(vision);

    m_textureScale  = 1.0f / (m_brickCount*m_brickSize);
    m_textureSubdivCount = 1;

    m_useMaterials = false;

    int dim = 0;

    dim = (m_mosaicCount*m_brickCount+1)*(m_mosaicCount*m_brickCount+1);
    std::vector<float>(dim).swap(m_relief);

    dim = m_mosaicCount*m_textureSubdivCount*m_mosaicCount*m_textureSubdivCount;
    std::vector<int>(dim).swap(m_textures);

    dim = m_mosaicCount*m_mosaicCount;
    std::vector<int>(dim, -1).swap(m_objRanks);

    return true;
}


int CTerrain::GetMosaicCount()
{
    return m_mosaicCount;
}

int CTerrain::GetBrickCount()
{
    return m_brickCount;
}

float CTerrain::GetBrickSize()
{
    return m_brickSize;
}

float CTerrain::GetReliefScale()
{
    return m_scaleRelief;
}

bool CTerrain::InitTextures(const std::string& baseName, int* table, int dx, int dy)
{
    m_useMaterials = false;

    m_texBaseName = baseName;
    auto pos = baseName.rfind('.');
    if(pos < baseName.find_last_of('/')) pos = std::string::npos; // If last . is not a part of filename (some directory, possibly . or ..)

    if (pos == std::string::npos)
    {
        m_texBaseExt = ".png";
    }
    else
    {
        m_texBaseExt = m_texBaseName.substr(pos);
        m_texBaseName = m_texBaseName.substr(0, pos);
    }

    for (int y = 0; y < m_mosaicCount*m_textureSubdivCount; y++)
    {
        for (int x = 0; x < m_mosaicCount*m_textureSubdivCount; x++)
        {
            m_textures[x+y*m_mosaicCount] = table[(x%dx)+(y%dy)*dx];
        }
    }
    return true;
}


void CTerrain::FlushMaterials()
{
    m_materials.clear();
    m_maxMaterialID = 0;
    m_materialAutoID = 1000;
    FlushMaterialPoints();
}

void CTerrain::AddMaterial(int id, const std::string& texName, const Math::Point &uv,
                           int up, int right, int down, int left,
                           float hardness)
{
    InitMaterialPoints();

    if (id == 0)
        id = m_materialAutoID++;

    TerrainMaterial tm;
    tm.texName  = texName;
    tm.id       = id;
    tm.uv       = uv;
    tm.mat[0]   = up;
    tm.mat[1]   = right;
    tm.mat[2]   = down;
    tm.mat[3]   = left;
    tm.hardness = hardness;

    m_materials.push_back(tm);

    if (m_maxMaterialID < up+1   )  m_maxMaterialID = up+1;
    if (m_maxMaterialID < right+1)  m_maxMaterialID = right+1;
    if (m_maxMaterialID < down+1 )  m_maxMaterialID = down+1;
    if (m_maxMaterialID < left+1 )  m_maxMaterialID = left+1;

    m_useMaterials = true;
    m_textureSubdivCount = 4;
}


// values from original bitmap palette
const std::map<TerrainRes, Gfx::IntColor> RESOURCE_PALETTE = {
    {TR_STONE,   Gfx::IntColor(255,   0, 0)},
    {TR_URANIUM, Gfx::IntColor(255, 255, 0)},
    {TR_POWER,   Gfx::IntColor(  0, 255, 0)},
    {TR_KEY_A,   Gfx::IntColor(  0, 204, 0)},
    {TR_KEY_B,   Gfx::IntColor( 51, 204, 0)},
    {TR_KEY_C,   Gfx::IntColor(102, 204, 0)},
    {TR_KEY_D,   Gfx::IntColor(153, 204, 0)}
};

Gfx::IntColor ResourceToColor(TerrainRes res)
{
    return RESOURCE_PALETTE.at(res);
}

/**
 * The image must be 24 bits/pixel and grayscale and dx x dy in size
 * with dx = dy = (mosaic*brick)+1 */
bool CTerrain::LoadResources(const std::string& fileName)
{
    CImage img;

    if (! img.Load(fileName))
    {
        GetLogger()->Error("Cannot load resource file: '%s'\n", fileName.c_str());
        return false;
    }

    ImageData *data = img.GetData();

    int size = (m_mosaicCount*m_brickCount)+1;

    std::vector<unsigned char>(size*size).swap(m_resources);

    if ( (data->surface->w != size) || (data->surface->h != size) )
    {
        GetLogger()->Error("Invalid resource file! Expected %dx%d\n", size, size);
        return false;
    }

    for (int x = 0; x < size; ++x)
    {
        for (int y = 0; y < size; ++y)
        {
            Gfx::IntColor pixel = img.GetPixelInt(Math::IntPoint(x, size - y - 1));
            TerrainRes res = TR_NULL;

            for (const auto& it : RESOURCE_PALETTE)
            {
                if (pixel.r == it.second.r && pixel.g == it.second.g && pixel.b == it.second.b)
                    res = it.first;
            }

            m_resources[x+size*y] = static_cast<unsigned char>(res);
        }
    }

    return true;
}

TerrainRes CTerrain::GetResource(const Math::Vector &pos)
{
    if (m_resources.empty())
        return TR_NULL;

    int x = static_cast<int>((pos.x + (m_mosaicCount*m_brickCount*m_brickSize)/2.0f)/m_brickSize);
    int y = static_cast<int>((pos.z + (m_mosaicCount*m_brickCount*m_brickSize)/2.0f)/m_brickSize);

    if ( x < 0 || x > m_mosaicCount*m_brickCount ||
         y < 0 || y > m_mosaicCount*m_brickCount )
        return TR_NULL;

    int size = (m_mosaicCount*m_brickCount)+1;

    return static_cast<TerrainRes>( m_resources[x+size*y] );
}

void CTerrain::FlushRelief()
{
    m_relief.clear();
    m_resources.clear();
    m_textures.clear();

    for (int objRank : m_objRanks)
    {
        if (objRank == -1)
            continue;

        int baseObjRank = m_engine->GetObjectBaseRank(objRank);

        if (baseObjRank != -1)
            m_engine->DeleteBaseObject(baseObjRank);

        m_engine->DeleteObject(objRank);
    }

    m_objRanks.clear();
}

/**
 * The image must be 24 bits/pixel and dx x dy in size
 * with dx = dy = (mosaic*brick)+1 */
bool CTerrain::LoadRelief(const std::string &fileName, float scaleRelief,
                          bool adjustBorder)
{
    m_scaleRelief = scaleRelief;

    CImage img;

    if (! img.Load(fileName))
    {
        GetLogger()->Error("Could not load relief file: '%s'!\n", fileName.c_str());
        return false;
    }

    ImageData *data = img.GetData();

    int size = (m_mosaicCount*m_brickCount)+1;
    GetLogger()->Debug("Expected relief size for current terrain configuration is %dx%d\n", size, size);

    if ( (data->surface->w != size) || (data->surface->h != size) )
    {
        GetLogger()->Error("Invalid relief file! Expected %dx%d\n", size, size);
        return false;
    }

    float limit = 0.9f;
    for (int y = 0; y < size; y++)
    {
        for (int x = 0; x < size; x++)
        {
            Gfx::IntColor color = img.GetPixelInt(Math::IntPoint(x, size - y - 1));

            float avg = (color.r + color.g + color.b) / 3.0f; // to be sure it is grayscale
            float level = (255.0f - avg) * scaleRelief;

            float dist = Math::Max(fabs(static_cast<float>(x-size/2)),
                                   fabs(static_cast<float>(y-size/2)));
            dist = dist/ static_cast<float>(size / 2);
            if (dist > limit && adjustBorder)
            {
                dist = (dist-limit)/(1.0f-limit);  // 0..1
                if (dist > 1.0f) dist = 1.0f;
                float border = 300.0f+Math::Rand()*20.0f;
                level = level+dist*(border-level);
            }

            m_relief[x+y*size] = level;
        }
    }

    return true;
}

bool CTerrain::RandomizeRelief()
{
    // Perlin noise
    // Based on Python implementation by Marek Rogalski (mafik)
    // http://amt2014.pl/archiwum/perlin.py

    int size = (m_mosaicCount*m_brickCount)+1;
    const int octaveCount = 6;

    std::unique_ptr<float[]> octaves[octaveCount];
    for(int i = 0; i < octaveCount; i++)
    {
        int pxCount = static_cast<int>(pow(2, (i+1)*2));
        octaves[i] = MakeUniqueArray<float>(pxCount);
        for(int j = 0; j < pxCount; j++)
        {
            octaves[i][j] = Math::Rand();
        }
    }

    for(int y2 = 0; y2 < size; y2++)
    {
        float y = static_cast<float>(y2) / size;
        for(int x2 = 0; x2 < size; x2++)
        {
            float x = static_cast<float>(x2) / size;

            float value = 0;
            for(int i = 0; i < octaveCount; i++)
            {
                int octaveSize = sqrt(static_cast<int>(pow(2, (i+1)*2)));
                double xi, yi, a, b;
                a = modf(x * (octaveSize-1), &xi);
                b = modf(y * (octaveSize-1), &yi);

                float lg = octaves[i][static_cast<int>(yi * octaveSize + xi)];
                float pg = octaves[i][static_cast<int>(yi * octaveSize + xi + 1)];
                float ld = octaves[i][static_cast<int>((yi+1) * octaveSize + xi)];
                float pd = octaves[i][static_cast<int>((yi+1) * octaveSize + xi + 1)];

                float g = pg * a + lg * (1-a);
                float d = pd * a + ld * (1-a);
                float res = d * b + g * (1-b);
                value += res;
            }

            value /= octaveCount;

            m_relief[x2+y2*size] = value * 255.0f;
        }
    }
    return true;
}

bool CTerrain::AddReliefPoint(Math::Vector pos, float scaleRelief)
{
    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;
    int size = (m_mosaicCount*m_brickCount)+1;

    pos.x = (pos.x+dim)/m_brickSize;
    pos.z = (pos.z+dim)/m_brickSize;

    int x = static_cast<int>(pos.x);
    int y = static_cast<int>(pos.z);

    if ( x < 0 || x >= size ||
         y < 0 || y >= size )  return false;

    if (m_relief[x+y*size] < pos.y*scaleRelief)
        m_relief[x+y*size] = pos.y*scaleRelief;

    return true;
}

void CTerrain::AdjustRelief()
{
    if (m_depth == 1) return;

    int ii = m_mosaicCount*m_brickCount+1;
    int b = 1 << (m_depth-1);

    for (int y = 0; y < m_mosaicCount*m_brickCount; y += b)
    {
        for (int x = 0; x < m_mosaicCount*m_brickCount; x += b)
        {
            int xx = 0;
            int yy = 0;
            if ((y+yy)%m_brickCount == 0)
            {
                float level1 = m_relief[(x+0)+(y+yy)*ii];
                float level2 = m_relief[(x+b)+(y+yy)*ii];
                for (xx = 1; xx < b; xx++)
                {
                    m_relief[(x+xx)+(y+yy)*ii] = ((level2-level1)/b)*xx+level1;
                }
            }

            yy = b;
            if ((y+yy)%m_brickCount == 0)
            {
                float level1 = m_relief[(x+0)+(y+yy)*ii];
                float level2 = m_relief[(x+b)+(y+yy)*ii];
                for (xx = 1; xx < b; xx++)
                {
                    m_relief[(x+xx)+(y+yy)*ii] = ((level2-level1)/b)*xx+level1;
                }
            }

            xx = 0;
            if ((x+xx)%m_brickCount == 0)
            {
                float level1 = m_relief[(x+xx)+(y+0)*ii];
                float level2 = m_relief[(x+xx)+(y+b)*ii];
                for (yy = 1; yy < b; yy++)
                {
                    m_relief[(x+xx)+(y+yy)*ii] = ((level2-level1)/b)*yy+level1;
                }
            }

            xx = b;
            if ((x+xx)%m_brickCount == 0)
            {
                float level1 = m_relief[(x+xx)+(y+0)*ii];
                float level2 = m_relief[(x+xx)+(y+b)*ii];
                for (yy = 1; yy < b; yy++)
                {
                    m_relief[(x+xx)+(y+yy)*ii] = ((level2-level1)/b)*yy+level1;
                }
            }
        }
    }
}

Math::Vector CTerrain::GetVector(int x, int y)
{
    Math::Vector p;
    p.x = x*m_brickSize - (m_mosaicCount*m_brickCount*m_brickSize) / 2.0;
    p.z = y*m_brickSize - (m_mosaicCount*m_brickCount*m_brickSize) / 2.0;

    if ( !m_relief.empty()                         &&
         x >= 0 && x <= m_mosaicCount*m_brickCount &&
         y >= 0 && y <= m_mosaicCount*m_brickCount )
    {
        p.y = m_relief[x+y*(m_mosaicCount*m_brickCount+1)];
    }
    else
    {
        p.y = 0.0f;
    }

    return p;
}

/** Calculates an averaged normal, taking into account the six adjacent triangles:

\verbatim
  ^ y
  |
  b---c---+
  |\  |\  |
  |  \|  \|
  a---o---d
  |\  |\  |
  |  \|  \|
  +---f---e--> x
\endverbatim */
VertexTex2 CTerrain::GetVertex(int x, int y, int step)
{
    VertexTex2 v;

    Math::Vector o = GetVector(x, y);
    v.coord = o;

    Math::Vector a = GetVector(x-step, y     );
    Math::Vector b = GetVector(x-step, y+step);
    Math::Vector c = GetVector(x,      y+step);
    Math::Vector d = GetVector(x+step, y     );
    Math::Vector e = GetVector(x+step, y-step);
    Math::Vector f = GetVector(x,      y-step);

    Math::Vector s(0.0f, 0.0f, 0.0f);

    if (x-step >= 0 && y+step <= m_mosaicCount*m_brickCount+1)
    {
        s += Math::NormalToPlane(b,a,o);
        s += Math::NormalToPlane(c,b,o);
    }

    if (x+step <= m_mosaicCount*m_brickCount+1 && y+step <= m_mosaicCount*m_brickCount+1)
        s += Math::NormalToPlane(d,c,o);

    if (x+step <= m_mosaicCount*m_brickCount+1 && y-step >= 0)
    {
        s += Math::NormalToPlane(e,d,o);
        s += Math::NormalToPlane(f,e,o);
    }

    if (x-step >= 0 && y-step >= 0)
        s += Math::NormalToPlane(a,f,o);

    s = Normalize(s);
    v.normal = s;

    int brick = m_brickCount/m_textureSubdivCount;
    Math::Vector oo = GetVector((x/brick)*brick, (y/brick)*brick);
    o  = GetVector(x, y);
    v.texCoord.x =        (o.x-oo.x)*m_textureScale*m_textureSubdivCount;
    v.texCoord.y = 1.0f - (o.z-oo.z)*m_textureScale*m_textureSubdivCount;

    v.texCoord2 = v.texCoord;

    return v;
}

/** The origin of mosaic is its center.
\verbatim
  ^ z
  |
  |  2---4---6--
  |  |\  |\  |\
  |  |  \|  \|
  |  1---3---5--- ...
  |
  +-------------------> x
\endverbatim */
bool CTerrain::CreateMosaic(int ox, int oy, int step, int objRank,
                            const Material &mat)
{
    int baseObjRank = m_engine->GetObjectBaseRank(objRank);
    if (baseObjRank == -1)
    {
        baseObjRank = m_engine->CreateBaseObject();
        m_engine->SetObjectBaseRank(objRank, baseObjRank);
    }

    std::string texName1;
    std::string texName2;

    if ( step == 1 )
    {
        int i = (ox/5) + (oy/5)*(m_mosaicCount/5);
        std::stringstream s;
        s << "shadow";
        s.width(2);
        s.fill('0');
        s << i;
        s << ".png";
        texName2 = s.str();
    }

    int brick = m_brickCount/m_textureSubdivCount;

    VertexTex2 o = GetVertex(ox*m_brickCount+m_brickCount/2, oy*m_brickCount+m_brickCount/2, step);
    int total = ((brick/step)+1)*2;

    float pixel = 1.0f/256.0f;  // 1 pixel cover (*)
    float dp = 1.0f/512.0f;

    Math::Point uv;

    for (int my = 0; my < m_textureSubdivCount; my++)
    {
        for (int mx = 0; mx < m_textureSubdivCount; mx++)
        {
            if (m_useMaterials)
            {
                int xx = ox*m_brickCount + mx*(m_brickCount/m_textureSubdivCount);
                int yy = oy*m_brickCount + my*(m_brickCount/m_textureSubdivCount);
                GetTexture(xx, yy, texName1, uv);
            }
            else
            {
                int i = (ox*m_textureSubdivCount+mx)+(oy*m_textureSubdivCount+my)*m_mosaicCount;
                std::stringstream s;
                s << m_texBaseName;
                s.width(3);
                s.fill('0');
                s << m_textures[i];
                s << m_texBaseExt;
                texName1 = s.str();
            }

            for (int y = 0; y < brick; y += step)
            {
                EngineBaseObjDataTier buffer;
                buffer.vertices.reserve(total);

                buffer.type = ENG_TRIANGLE_TYPE_SURFACE;
                buffer.material = mat;

                buffer.state = ENG_RSTATE_WRAP;

                buffer.state |= ENG_RSTATE_SECOND;
                if (step == 1)
                    buffer.state |= ENG_RSTATE_DUAL_BLACK;

                for (int x = 0; x <= brick; x += step)
                {
                    VertexTex2 p1 = GetVertex(ox*m_brickCount+mx*brick+x, oy*m_brickCount+my*brick+y+0   , step);
                    VertexTex2 p2 = GetVertex(ox*m_brickCount+mx*brick+x, oy*m_brickCount+my*brick+y+step, step);
                    p1.coord.x -= o.coord.x;  p1.coord.z -= o.coord.z;
                    p2.coord.x -= o.coord.x;  p2.coord.z -= o.coord.z;

                    // TODO: Find portable solution
                    //float offset = 0.5f / 256.0f;      // Direct3D
                    float offset = 0.0f;                 // OpenGL

                    if (x == 0)
                    {
                        p1.texCoord.x = 0.0f + offset;
                        p2.texCoord.x = 0.0f + offset;
                    }
                    if (x == brick)
                    {
                        p1.texCoord.x = 1.0f - offset;
                        p2.texCoord.x = 1.0f - offset;
                    }
                    if (y == 0)
                        p1.texCoord.y = 1.0f - offset;

                    if (y == brick - step)
                        p2.texCoord.y = 0.0f + offset;

                    if (m_useMaterials)
                    {
                        p1.texCoord.x /= m_textureSubdivCount;  // 0..1 -> 0..0.25
                        p1.texCoord.y /= m_textureSubdivCount;
                        p2.texCoord.x /= m_textureSubdivCount;
                        p2.texCoord.y /= m_textureSubdivCount;

                        if (x == 0)
                        {
                            p1.texCoord.x = 0.0f+dp;
                            p2.texCoord.x = 0.0f+dp;
                        }
                        if (x == brick)
                        {
                            p1.texCoord.x = (1.0f/m_textureSubdivCount)-dp;
                            p2.texCoord.x = (1.0f/m_textureSubdivCount)-dp;
                        }
                        if (y == 0)
                            p1.texCoord.y = (1.0f/m_textureSubdivCount)-dp;

                        if (y == brick - step)
                            p2.texCoord.y = 0.0f+dp;

                        p1.texCoord.x += uv.x;
                        p1.texCoord.y += uv.y;
                        p2.texCoord.x += uv.x;
                        p2.texCoord.y += uv.y;
                    }

                    int xx = mx*(m_brickCount/m_textureSubdivCount) + x;
                    int yy = my*(m_brickCount/m_textureSubdivCount) + y;
                    p1.texCoord2.x = (static_cast<float>(ox%5)*m_brickCount+xx+0.0f)/(m_brickCount*5);
                    p1.texCoord2.y = (static_cast<float>(oy%5)*m_brickCount+yy+0.0f)/(m_brickCount*5);
                    p2.texCoord2.x = (static_cast<float>(ox%5)*m_brickCount+xx+0.0f)/(m_brickCount*5);
                    p2.texCoord2.y = (static_cast<float>(oy%5)*m_brickCount+yy+1.0f)/(m_brickCount*5);

// Correction for 1 pixel cover
// There is 1 pixel cover around each of the 16 surfaces:
//
//  |<--------------256-------------->|
//  |   |<----------254---------->|   |
//  |---|---|---|-- ... --|---|---|---|
//    |  0.0                   1.0  |
//    |   |                     |   |
//   0.0 min                   max 1.0
//
// The uv coordinates used for texturing are between min and max (instead of 0 and 1)
// This allows to exclude the pixels situated in a margin of a pixel around the surface

                    p1.texCoord2.x = (p1.texCoord2.x+pixel)*(1.0f-pixel)/(1.0f+pixel);
                    p1.texCoord2.y = (p1.texCoord2.y+pixel)*(1.0f-pixel)/(1.0f+pixel);
                    p2.texCoord2.x = (p2.texCoord2.x+pixel)*(1.0f-pixel)/(1.0f+pixel);
                    p2.texCoord2.y = (p2.texCoord2.y+pixel)*(1.0f-pixel)/(1.0f+pixel);


                    buffer.vertices.push_back(p1);
                    buffer.vertices.push_back(p2);
                }

                m_engine->AddBaseObjQuick(baseObjRank, buffer, texName1, texName2, true);
            }
        }
    }

    Math::Matrix transform;
    transform.LoadIdentity();
    transform.Set(1, 4, o.coord.x);
    transform.Set(3, 4, o.coord.z);
    m_engine->SetObjectTransform(objRank, transform);

    return true;
}

CTerrain::TerrainMaterial* CTerrain::FindMaterial(int id)
{
    for (int i = 0; i < static_cast<int>( m_materials.size() ); i++)
    {
        if (id == m_materials[i].id)
            return &m_materials[i];
    }

    return nullptr;
}

void CTerrain::GetTexture(int x, int y, std::string& name, Math::Point &uv)
{
    x /= m_brickCount/m_textureSubdivCount;
    y /= m_brickCount/m_textureSubdivCount;

    TerrainMaterial* tm = FindMaterial(m_materialPoints[x+y*m_materialPointCount].id);
    if (tm == nullptr)
    {
        name = "";
        uv = Math::Point(0.0f, 0.0f);
    }
    else
    {
        name = tm->texName;
        uv = tm->uv;
    }
}

float CTerrain::GetHeight(int x, int y)
{
    int size = (m_mosaicCount*m_brickCount+1);

    if (x <  0   )  x = 0;
    if (x >= size)  x = size-1;
    if (y <  0   )  y = 0;
    if (y >= size)  y = size-1;

    return m_relief[x+y*size];
}

bool CTerrain::CheckMaterialPoint(int x, int y, float min, float max, float slope)
{
    float hc = GetHeight(x, y);
    float h[4] =
    {
        GetHeight(x+0, y+1),
        GetHeight(x+1, y+0),
        GetHeight(x+0, y-1),
        GetHeight(x-1, y+0)
    };

    if (hc < min || hc > max)
        return false;

    if (slope == 0.0f)
        return true;

    if (slope > 0.0f)
    {
        for (int i = 0; i < 4; i++)
        {
            if (fabs(hc - h[i]) >= slope)
                return false;
        }
        return true;
    }

    if (slope < 0.0f)
    {
        for (int i = 0; i < 4; i++)
        {
            if (fabs(hc - h[i]) < -slope)
                return false;
        }
        return true;
    }

    return false;
}

int CTerrain::FindMaterialByNeighbors(char *mat)
{
    for (int i = 0; i < static_cast<int>( m_materials.size() ); i++)
    {
        if ( m_materials[i].mat[0] == mat[0] &&
             m_materials[i].mat[1] == mat[1] &&
             m_materials[i].mat[2] == mat[2] &&
             m_materials[i].mat[3] == mat[3] )  return i;
    }

    return -1;
}

void CTerrain::SetMaterialPoint(int x, int y, int id, char *mat)
{
    TerrainMaterial* tm = FindMaterial(id);
    if (tm == nullptr)  return;

    if ( tm->mat[0] != mat[0] ||
         tm->mat[1] != mat[1] ||
         tm->mat[2] != mat[2] ||
         tm->mat[3] != mat[3] )  // id incompatible with mat?
    {
        int ii = FindMaterialByNeighbors(mat);
        if (ii == -1)  return;
        id = m_materials[ii].id;  // looking for a id compatible with mat
    }

    // Changes the point
    m_materialPoints[x+y*m_materialPointCount].id     = id;
    m_materialPoints[x+y*m_materialPointCount].mat[0] = mat[0];
    m_materialPoints[x+y*m_materialPointCount].mat[1] = mat[1];
    m_materialPoints[x+y*m_materialPointCount].mat[2] = mat[2];
    m_materialPoints[x+y*m_materialPointCount].mat[3] = mat[3];

    // Changes the lower neighbor
    if ( (x+0) >= 0 && (x+0) < m_materialPointCount &&
         (y-1) >= 0 && (y-1) < m_materialPointCount )
    {
        int i = (x+0)+(y-1)*m_materialPointCount;
        if (m_materialPoints[i].mat[0] != mat[2])
        {
            m_materialPoints[i].mat[0] = mat[2];
            int ii = FindMaterialByNeighbors(m_materialPoints[i].mat);
            if (ii != -1)
                m_materialPoints[i].id = m_materials[ii].id;
        }
    }

    // Modifies the left neighbor
    if ( (x-1) >= 0 && (x-1) < m_materialPointCount &&
         (y+0) >= 0 && (y+0) < m_materialPointCount )
    {
        int i = (x-1)+(y+0)*m_materialPointCount;
        if (m_materialPoints[i].mat[1] != mat[3])
        {
            m_materialPoints[i].mat[1] = mat[3];
            int ii = FindMaterialByNeighbors(m_materialPoints[i].mat);
            if (ii != -1)
                m_materialPoints[i].id = m_materials[ii].id;
        }
    }

    // Changes the upper neighbor
    if ( (x+0) >= 0 && (x+0) < m_materialPointCount &&
         (y+1) >= 0 && (y+1) < m_materialPointCount )
    {
        int i = (x+0)+(y+1)*m_materialPointCount;
        if (m_materialPoints[i].mat[2] != mat[0])
        {
            m_materialPoints[i].mat[2] = mat[0];
            int ii = FindMaterialByNeighbors(m_materialPoints[i].mat);
            if (ii != -1)
                m_materialPoints[i].id = m_materials[ii].id;
        }
    }

    // Changes the right neighbor
    if ( (x+1) >= 0 && (x+1) < m_materialPointCount &&
         (y+0) >= 0 && (y+0) < m_materialPointCount )
    {
        int i = (x+1)+(y+0)*m_materialPointCount;
        if ( m_materialPoints[i].mat[3] != mat[1] )
        {
            m_materialPoints[i].mat[3] = mat[1];
            int ii = FindMaterialByNeighbors(m_materialPoints[i].mat);
            if (ii != -1)
                m_materialPoints[i].id = m_materials[ii].id;
        }
    }
}

bool CTerrain::CondChangeMaterialPoint(int x, int y, int id, char *mat)
{
    char test[4];

    // Compatible with lower neighbor?
    if ( x+0 >= 0 && x+0 < m_materialPointCount &&
         y-1 >= 0 && y-1 < m_materialPointCount )
    {
        test[0] = mat[2];
        test[1] = m_materialPoints[(x+0)+(y-1)*m_materialPointCount].mat[1];
        test[2] = m_materialPoints[(x+0)+(y-1)*m_materialPointCount].mat[2];
        test[3] = m_materialPoints[(x+0)+(y-1)*m_materialPointCount].mat[3];

        if ( FindMaterialByNeighbors(test) == -1 )  return false;
    }

    // Compatible with left neighbor?
    if ( x-1 >= 0 && x-1 < m_materialPointCount &&
         y+0 >= 0 && y+0 < m_materialPointCount )
    {
        test[0] = m_materialPoints[(x-1)+(y+0)*m_materialPointCount].mat[0];
        test[1] = mat[3];
        test[2] = m_materialPoints[(x-1)+(y+0)*m_materialPointCount].mat[2];
        test[3] = m_materialPoints[(x-1)+(y+0)*m_materialPointCount].mat[3];

        if ( FindMaterialByNeighbors(test) == -1 )  return false;
    }

    // Compatible with upper neighbor?
    if ( x+0 >= 0 && x+0 < m_materialPointCount &&
         y+1 >= 0 && y+1 < m_materialPointCount )
    {
        test[0] = m_materialPoints[(x+0)+(y+1)*m_materialPointCount].mat[0];
        test[1] = m_materialPoints[(x+0)+(y+1)*m_materialPointCount].mat[1];
        test[2] = mat[0];
        test[3] = m_materialPoints[(x+0)+(y+1)*m_materialPointCount].mat[3];

        if ( FindMaterialByNeighbors(test) == -1 )  return false;
    }

    // Compatible with right neighbor?
    if ( x+1 >= 0 && x+1 < m_materialPointCount &&
         y+0 >= 0 && y+0 < m_materialPointCount )
    {
        test[0] = m_materialPoints[(x+1)+(y+0)*m_materialPointCount].mat[0];
        test[1] = m_materialPoints[(x+1)+(y+0)*m_materialPointCount].mat[1];
        test[2] = m_materialPoints[(x+1)+(y+0)*m_materialPointCount].mat[2];
        test[3] = mat[1];

        if ( FindMaterialByNeighbors(test) == -1 )  return false;
    }

    SetMaterialPoint(x, y, id, mat);  // puts the point
    return true;
}

bool CTerrain::ChangeMaterialPoint(int x, int y, int id)
{
    char mat[4];

    x /= m_brickCount/m_textureSubdivCount;
    y /= m_brickCount/m_textureSubdivCount;

    if ( x < 0 || x >= m_materialPointCount ||
         y < 0 || y >= m_materialPointCount )  return false;

    TerrainMaterial* tm = FindMaterial(id);
    if (tm == nullptr)  return false;

    // Tries without changing neighbors.
    if ( CondChangeMaterialPoint(x, y, id, tm->mat) )  return true;

    // Tries changing a single neighbor (4x).
    for (int up = 0; up < m_maxMaterialID; up++)
    {
        mat[0] = up;
        mat[1] = tm->mat[1];
        mat[2] = tm->mat[2];
        mat[3] = tm->mat[3];

        if (CondChangeMaterialPoint(x, y, id, mat))  return true;
    }

    for (int right = 0; right < m_maxMaterialID; right++)
    {
        mat[0] = tm->mat[0];
        mat[1] = right;
        mat[2] = tm->mat[2];
        mat[3] = tm->mat[3];

        if (CondChangeMaterialPoint(x, y, id, mat)) return true;
    }

    for (int down = 0; down < m_maxMaterialID; down++)
    {
        mat[0] = tm->mat[0];
        mat[1] = tm->mat[1];
        mat[2] = down;
        mat[3] = tm->mat[3];

        if (CondChangeMaterialPoint(x, y, id, mat)) return true;
    }

    for (int left = 0; left < m_maxMaterialID; left++)
    {
        mat[0] = tm->mat[0];
        mat[1] = tm->mat[1];
        mat[2] = tm->mat[2];
        mat[3] = left;

        if (CondChangeMaterialPoint(x, y, id, mat)) return true;
    }

    // Tries changing two neighbors (6x).
    for (int up = 0; up < m_maxMaterialID; up++)
    {
        for (int down = 0; down < m_maxMaterialID; down++)
        {
            mat[0] = up;
            mat[1] = tm->mat[1];
            mat[2] = down;
            mat[3] = tm->mat[3];

            if (CondChangeMaterialPoint(x, y, id, mat)) return true;
        }
    }

    for (int right = 0; right < m_maxMaterialID; right++)
    {
        for (int left = 0; left < m_maxMaterialID; left++)
        {
            mat[0] = tm->mat[0];
            mat[1] = right;
            mat[2] = tm->mat[2];
            mat[3] = left;

            if (CondChangeMaterialPoint(x, y, id, mat)) return true;
        }
    }

    for (int up = 0; up < m_maxMaterialID; up++)
    {
        for (int right = 0; right < m_maxMaterialID; right++)
        {
            mat[0] = up;
            mat[1] = right;
            mat[2] = tm->mat[2];
            mat[3] = tm->mat[3];

            if (CondChangeMaterialPoint(x, y, id, mat)) return true;
        }
    }

    for (int right = 0; right < m_maxMaterialID; right++)
    {
        for (int down = 0; down < m_maxMaterialID; down++)
        {
            mat[0] = tm->mat[0];
            mat[1] = right;
            mat[2] = down;
            mat[3] = tm->mat[3];

            if (CondChangeMaterialPoint(x, y, id, mat)) return true;
        }
    }

    for (int down = 0; down < m_maxMaterialID; down++)
    {
        for (int left = 0; left < m_maxMaterialID; left++)
        {
            mat[0] = tm->mat[0];
            mat[1] = tm->mat[1];
            mat[2] = down;
            mat[3] = left;

            if (CondChangeMaterialPoint(x, y, id, mat)) return true;
        }
    }

    for (int up = 0; up < m_maxMaterialID; up++)
    {
        for (int left = 0; left < m_maxMaterialID; left++)
        {
            mat[0] = up;
            mat[1] = tm->mat[1];
            mat[2] = tm->mat[2];
            mat[3] = left;

            if (CondChangeMaterialPoint(x, y, id, mat)) return true;
        }
    }

    // Tries changing all the neighbors.
    for (int up = 0; up < m_maxMaterialID; up++)
    {
        for (int right = 0; right < m_maxMaterialID; right++)
        {
            for (int down = 0; down < m_maxMaterialID; down++)
            {
                for (int left = 0; left < m_maxMaterialID; left++)
                {
                    mat[0] = up;
                    mat[1] = right;
                    mat[2] = down;
                    mat[3] = left;

                    if (CondChangeMaterialPoint(x, y, id, mat)) return true;
                }
            }
        }
    }

    GetLogger()->Error("AddMaterialPoint error\n");
    return false;
}

bool CTerrain::InitMaterials(int id)
{
    TerrainMaterial* tm = FindMaterial(id);
    if (tm == nullptr) return false;

    for (int i = 0; i < m_materialPointCount*m_materialPointCount; i++)
    {
        m_materialPoints[i].id = id;

        for (int j = 0; j < 4; j++)
            m_materialPoints[i].mat[j] = tm->mat[j];
    }

    return true;
}

bool CTerrain::GenerateMaterials(int *id, float min, float max,
                                 float slope, float freq,
                                 Math::Vector center, float radius)
{
    static char random[100] =
    {
        84,25,12, 6,34,52,85,38,97,16,
        21,31,65,19,62,40,72,22,48,61,
        56,47, 8,53,73,77, 4,91,26,88,
        76, 1,44,93,39,11,71,17,98,95,
        88,83,18,30, 3,57,28,49,74, 9,
        32,13,96,66,15,70,36,10,59,94,
        45,86, 2,29,63,42,51, 0,79,27,
        54, 7,20,69,89,23,64,43,81,92,
        90,33,46,14,67,35,50, 5,87,60,
        68,55,24,78,41,75,58,80,37,82,
    };

    TerrainMaterial* tm = nullptr;

    int i = 0;
    while (id[i] != 0)
    {
        tm = FindMaterial(id[i++]);
        if (tm == nullptr) return false;
    }
    int numID = i;

    int group = m_brickCount / m_textureSubdivCount;

    if (radius > 0.0f && radius < 5.0f)  // just a square?
    {
        float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

        int xx = static_cast<int>((center.x+dim)/m_brickSize);
        int yy = static_cast<int>((center.z+dim)/m_brickSize);

        int x = xx/group;
        int y = yy/group;

        tm = FindMaterial(id[0]);
        if (tm != nullptr)
            SetMaterialPoint(x, y, id[0], tm->mat);  // puts the point
    }
    else
    {
        for (int y = 0; y < m_materialPointCount; y++)
        {
            for (int x = 0; x < m_materialPointCount; x++)
            {
                if (radius != 0.0f)
                {
                    Math::Vector pos;
                    pos.x = (static_cast<float>(x)-m_materialPointCount/2.0f)*group*m_brickSize;
                    pos.z = (static_cast<float>(y)-m_materialPointCount/2.0f)*group*m_brickSize;
                    if (Math::DistanceProjected(pos, center) > radius) continue;
                }

                if (freq < 100.0f)
                {
                    int rnd = random[(x%10)+(y%10)*10];
                    if ( static_cast<float>(rnd) > freq )  continue;
                }

                int xx = x*group + group/2;
                int yy = y*group + group/2;

                if (CheckMaterialPoint(xx, yy, min, max, slope))
                {
                    int rnd = random[(x%10)+(y%10)*10];
                    int ii = rnd % numID;
                    ChangeMaterialPoint(xx, yy, id[ii]);
                }
            }
        }
    }

    return true;
}

void CTerrain::InitMaterialPoints()
{
    if (! m_useMaterials) return;
    if (! m_materialPoints.empty()) return; // already allocated

    m_materialPointCount = (m_mosaicCount*m_brickCount)/(m_brickCount/m_textureSubdivCount)+1;
    std::vector<TerrainMaterialPoint>(m_materialPointCount*m_materialPointCount).swap(m_materialPoints);

    for (int i = 0; i < m_materialPointCount * m_materialPointCount; i++)
    {
        for (int j = 0; j < 4; j++)
            m_materialPoints[i].mat[j] = 0;
    }
}

void CTerrain::FlushMaterialPoints()
{
    m_materialPoints.clear();
}

bool CTerrain::CreateSquare(int x, int y)
{
    Material mat;
    mat.diffuse = Color(1.0f, 1.0f, 1.0f);
    mat.ambient = Color(0.0f, 0.0f, 0.0f);

    int objRank = m_engine->CreateObject();
    m_engine->SetObjectType(objRank, ENG_OBJTYPE_TERRAIN);

    m_objRanks[x+y*m_mosaicCount] = objRank;

    for (int step = 0; step < m_depth; step++)
    {
        CreateMosaic(x, y, 1 << step, objRank, mat);
    }

    return true;
}

bool CTerrain::CreateObjects()
{
    AdjustRelief();

    for (int y = 0; y < m_mosaicCount; y++)
    {
        for (int x = 0; x < m_mosaicCount; x++)
            CreateSquare(x, y);
    }

    return true;
}

/** ATTENTION: ok only with m_depth = 2! */
bool CTerrain::Terraform(const Math::Vector &p1, const Math::Vector &p2, float height)
{
    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

    Math::IntPoint tp1, tp2;
    tp1.x = static_cast<int>((p1.x+dim+m_brickSize/2.0f)/m_brickSize);
    tp1.y = static_cast<int>((p1.z+dim+m_brickSize/2.0f)/m_brickSize);
    tp2.x = static_cast<int>((p2.x+dim+m_brickSize/2.0f)/m_brickSize);
    tp2.y = static_cast<int>((p2.z+dim+m_brickSize/2.0f)/m_brickSize);

    if (tp1.x > tp2.x)
    {
        int x = tp1.x;
        tp1.x = tp2.x;
        tp2.x = x;
    }

    if (tp1.y > tp2.y)
    {
        int y = tp1.y;
        tp1.y = tp2.y;
        tp2.y = y;
    }

    int size = (m_mosaicCount*m_brickCount)+1;

    // Calculates the current average height
    float avg = 0.0f;
    int nb = 0;
    for (int y = tp1.y; y <= tp2.y; y++)
    {
        for (int x = tp1.x; x <= tp2.x; x++)
        {
            avg += m_relief[x+y*size];
            nb ++;
        }
    }
    avg /= static_cast<float>(nb);

    // Changes the description of the relief
    for (int y = tp1.y; y <= tp2.y; y++)
    {
        for (int x = tp1.x; x <= tp2.x; x++)
        {
            m_relief[x+y*size] = avg+height;

            if (x % m_brickCount == 0 && y % m_depth != 0)
            {
                m_relief[(x+0)+(y-1)*size] = avg+height;
                m_relief[(x+0)+(y+1)*size] = avg+height;
            }

            if (y % m_brickCount == 0 && x % m_depth != 0)
            {
                m_relief[(x-1)+(y+0)*size] = avg+height;
                m_relief[(x+1)+(y+0)*size] = avg+height;
            }
        }
    }
    AdjustRelief();

    Math::IntPoint pp1, pp2;
    pp1.x = (tp1.x-2)/m_brickCount;
    pp1.y = (tp1.y-2)/m_brickCount;
    pp2.x = (tp2.x+1)/m_brickCount;
    pp2.y = (tp2.y+1)/m_brickCount;

    if (pp1.x <  0            ) pp1.x = 0;
    if (pp1.x >= m_mosaicCount) pp1.x = m_mosaicCount-1;
    if (pp1.y <  0            ) pp1.y = 0;
    if (pp1.y >= m_mosaicCount) pp1.y = m_mosaicCount-1;

    for (int y = pp1.y; y <= pp2.y; y++)
    {
        for (int x = pp1.x; x <= pp2.x; x++)
        {
            int objRank = m_objRanks[x+y*m_mosaicCount];
            int baseObjRank = m_engine->GetObjectBaseRank(objRank);
            m_engine->DeleteBaseObject(baseObjRank);
            m_engine->DeleteObject(objRank);
            CreateSquare(x, y);  // recreates the square
        }
    }
    m_engine->Update();

    return true;
}

void CTerrain::SetWind(Math::Vector speed)
{
    m_wind = speed;
}

Math::Vector CTerrain::GetWind()
{
    return m_wind;
}

float CTerrain::GetFineSlope(const Math::Vector &pos)
{
    Math::Vector n;
    if (! GetNormal(n, pos)) return 0.0f;
    return fabs(Math::RotateAngle(Math::Point(n.x, n.z).Length(), n.y) - Math::PI/2.0f);
}

float CTerrain::GetCoarseSlope(const Math::Vector &pos)
{
    if (m_relief.empty()) return 0.0f;

    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

    int x = static_cast<int>((pos.x+dim)/m_brickSize);
    int y = static_cast<int>((pos.z+dim)/m_brickSize);

    if ( x < 0 || x >= m_mosaicCount*m_brickCount ||
         y < 0 || y >= m_mosaicCount*m_brickCount ) return 0.0f;

    float level[4] =
    {
        m_relief[(x+0)+(y+0)*(m_mosaicCount*m_brickCount+1)],
        m_relief[(x+1)+(y+0)*(m_mosaicCount*m_brickCount+1)],
        m_relief[(x+0)+(y+1)*(m_mosaicCount*m_brickCount+1)],
        m_relief[(x+1)+(y+1)*(m_mosaicCount*m_brickCount+1)],
    };

    float min = Math::Min(level[0], level[1], level[2], level[3]);
    float max = Math::Max(level[0], level[1], level[2], level[3]);

    return atanf((max-min)/m_brickSize);
}

bool CTerrain::GetNormal(Math::Vector &n, const Math::Vector &p)
{
    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

    int x = static_cast<int>((p.x+dim)/m_brickSize);
    int y = static_cast<int>((p.z+dim)/m_brickSize);

    if ( x < 0 || x > m_mosaicCount*m_brickCount ||
         y < 0 || y > m_mosaicCount*m_brickCount )  return false;

    Math::Vector p1 = GetVector(x+0, y+0);
    Math::Vector p2 = GetVector(x+1, y+0);
    Math::Vector p3 = GetVector(x+0, y+1);
    Math::Vector p4 = GetVector(x+1, y+1);

    if ( fabs(p.z - p2.z) < fabs(p.x - p2.x) )
        n = Math::NormalToPlane(p1,p2,p3);
    else
        n = Math::NormalToPlane(p2,p4,p3);

    return true;
}

float CTerrain::GetFloorLevel(const Math::Vector &pos, bool brut, bool water)
{
    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

    int x = static_cast<int>((pos.x+dim)/m_brickSize);
    int y = static_cast<int>((pos.z+dim)/m_brickSize);

    if ( x < 0 || x > m_mosaicCount*m_brickCount ||
         y < 0 || y > m_mosaicCount*m_brickCount )  return false;

    Math::Vector p1 = GetVector(x+0, y+0);
    Math::Vector p2 = GetVector(x+1, y+0);
    Math::Vector p3 = GetVector(x+0, y+1);
    Math::Vector p4 = GetVector(x+1, y+1);

    Math::Vector ps = pos;
    if ( fabs(pos.z-p2.z) < fabs(pos.x-p2.x) )
    {
        if ( !IntersectY(p1, p2, p3, ps) )  return 0.0f;
    }
    else
    {
        if ( !IntersectY(p2, p4, p3, ps) )  return 0.0f;
    }

    if (! brut) AdjustBuildingLevel(ps);

    if (water)  // not going underwater?
    {
        float level = m_water->GetLevel();
        if (ps.y < level) ps.y = level;  // not under water
    }

    return ps.y;
}

float CTerrain::GetHeightToFloor(const Math::Vector &pos, bool brut, bool water)
{
    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

    int x = static_cast<int>((pos.x+dim)/m_brickSize);
    int y = static_cast<int>((pos.z+dim)/m_brickSize);

    if ( x < 0 || x > m_mosaicCount*m_brickCount ||
         y < 0 || y > m_mosaicCount*m_brickCount )  return false;

    Math::Vector p1 = GetVector(x+0, y+0);
    Math::Vector p2 = GetVector(x+1, y+0);
    Math::Vector p3 = GetVector(x+0, y+1);
    Math::Vector p4 = GetVector(x+1, y+1);

    Math::Vector ps = pos;
    if ( fabs(pos.z-p2.z) < fabs(pos.x-p2.x) )
    {
        if ( !IntersectY(p1, p2, p3, ps) )  return 0.0f;
    }
    else
    {
        if ( !IntersectY(p2, p4, p3, ps) )  return 0.0f;
    }

    if (! brut) AdjustBuildingLevel(ps);

    if (water)  // not going underwater?
    {
        float level = m_water->GetLevel();
        if (ps.y < level ) ps.y = level;  // not under water
    }

    return pos.y-ps.y;
}

bool CTerrain::AdjustToFloor(Math::Vector &pos, bool brut, bool water)
{
    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

    int x = static_cast<int>((pos.x + dim) / m_brickSize);
    int y = static_cast<int>((pos.z + dim) / m_brickSize);

    if ( x < 0 || x > m_mosaicCount*m_brickCount ||
         y < 0 || y > m_mosaicCount*m_brickCount )  return false;

    Math::Vector p1 = GetVector(x+0, y+0);
    Math::Vector p2 = GetVector(x+1, y+0);
    Math::Vector p3 = GetVector(x+0, y+1);
    Math::Vector p4 = GetVector(x+1, y+1);

    if (fabs(pos.z - p2.z) < fabs(pos.x - p2.x))
    {
        if (! Math::IntersectY(p1, p2, p3, pos)) return false;
    }
    else
    {
        if (! Math::IntersectY(p2, p4, p3, pos)) return false;
    }

    if (! brut) AdjustBuildingLevel(pos);

    if (water)  // not going underwater?
    {
        float level = m_water->GetLevel();
        if (pos.y < level) pos.y = level;  // not under water
    }

    return true;
}

/**
 * \param pos position to adjust
 * \returns \c false if the initial coordinate was outside terrain area; \c true otherwise
 */
bool CTerrain::AdjustToStandardBounds(Math::Vector& pos)
{
    bool ok = true;

    // In _TEEN there used to be a limit of 0.98f
    float limit = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f*0.92f;

    if (pos.x < -limit)
    {
        pos.x = -limit;
        ok = false;
    }

    if (pos.z < -limit)
    {
        pos.z = -limit;
        ok = false;
    }

    if (pos.x > limit)
    {
        pos.x = limit;
        ok = false;
    }

    if (pos.z > limit)
    {
        pos.z = limit;
        ok = false;
    }

    return ok;
}

/**
 * \param pos position to adjust
 * \param margin margin to the terrain border
 * \returns \c false if the initial coordinate was outside terrain area; \c true otherwise
 */
bool CTerrain::AdjustToBounds(Math::Vector& pos, float margin)
{
    bool ok = true;
    float limit = m_mosaicCount*m_brickCount*m_brickSize/2.0f - margin;

    if (pos.x < -limit)
    {
        pos.x = -limit;
        ok = false;
    }

    if (pos.z < -limit)
    {
        pos.z = -limit;
        ok = false;
    }

    if (pos.x > limit)
    {
        pos.x = limit;
        ok = false;
    }

    if (pos.z > limit)
    {
        pos.z = limit;
        ok = false;
    }

    return ok;
}

void CTerrain::FlushBuildingLevel()
{
    m_buildingLevels.clear();
}

bool CTerrain::AddBuildingLevel(Math::Vector center, float min, float max,
                                     float height, float factor)
{
    int i = 0;
    for ( ; i < static_cast<int>( m_buildingLevels.size() ); i++)
    {
        if ( center.x == m_buildingLevels[i].center.x &&
             center.z == m_buildingLevels[i].center.z )
        {
            break;
        }
    }

    if (i == static_cast<int>( m_buildingLevels.size() ))
        m_buildingLevels.push_back(BuildingLevel());

    m_buildingLevels[i].center   = center;
    m_buildingLevels[i].min      = min;
    m_buildingLevels[i].max      = max;
    m_buildingLevels[i].level    = GetFloorLevel(center, true);
    m_buildingLevels[i].height   = height;
    m_buildingLevels[i].factor   = factor;
    m_buildingLevels[i].bboxMinX = center.x-max;
    m_buildingLevels[i].bboxMaxX = center.x+max;
    m_buildingLevels[i].bboxMinZ = center.z-max;
    m_buildingLevels[i].bboxMaxZ = center.z+max;

    return true;
}

bool CTerrain::UpdateBuildingLevel(Math::Vector center)
{
    for (int i = 0; i < static_cast<int>( m_buildingLevels.size() ); i++)
    {
        if ( center.x == m_buildingLevels[i].center.x &&
             center.z == m_buildingLevels[i].center.z )
        {
            m_buildingLevels[i].center = center;
            m_buildingLevels[i].level  = GetFloorLevel(center, true);
            return true;
        }
    }
    return false;
}

bool CTerrain::DeleteBuildingLevel(Math::Vector center)
{
    for (int i = 0; i < static_cast<int>( m_buildingLevels.size() ); i++)
    {
        if ( center.x == m_buildingLevels[i].center.x &&
             center.z == m_buildingLevels[i].center.z )
        {
            for (int j = i+1; j < static_cast<int>( m_buildingLevels.size() ); j++)
                m_buildingLevels[j-1] = m_buildingLevels[j];

            m_buildingLevels.pop_back();
            return true;
        }
    }
    return false;
}

float CTerrain::GetBuildingFactor(const Math::Vector &pos)
{
    for (int i = 0; i < static_cast<int>( m_buildingLevels.size() ); i++)
    {
        if ( pos.x < m_buildingLevels[i].bboxMinX ||
             pos.x > m_buildingLevels[i].bboxMaxX ||
             pos.z < m_buildingLevels[i].bboxMinZ ||
             pos.z > m_buildingLevels[i].bboxMaxZ )  continue;

        float dist = Math::DistanceProjected(pos, m_buildingLevels[i].center);

        if (dist <= m_buildingLevels[i].max)
            return m_buildingLevels[i].factor;
    }
    return 1.0f;  // it is normal on the ground
}

void CTerrain::AdjustBuildingLevel(Math::Vector &p)
{
    for (int i = 0; i < static_cast<int>( m_buildingLevels.size() ); i++)
    {
        if ( p.x < m_buildingLevels[i].bboxMinX ||
             p.x > m_buildingLevels[i].bboxMaxX ||
             p.z < m_buildingLevels[i].bboxMinZ ||
             p.z > m_buildingLevels[i].bboxMaxZ ) continue;

        float dist = Math::DistanceProjected(p, m_buildingLevels[i].center);

        if (dist > m_buildingLevels[i].max) continue;

        if (dist < m_buildingLevels[i].min)
        {
            p.y = m_buildingLevels[i].level + m_buildingLevels[i].height;
            return;
        }

        Math::Vector border;
        border.x = ((p.x - m_buildingLevels[i].center.x) * m_buildingLevels[i].max) /
                   dist + m_buildingLevels[i].center.x;
        border.z = ((p.z - m_buildingLevels[i].center.z) * m_buildingLevels[i].max) /
                   dist + m_buildingLevels[i].center.z;

        float base = GetFloorLevel(border, true);

        p.y = (m_buildingLevels[i].max - dist) /
              (m_buildingLevels[i].max - m_buildingLevels[i].min) *
              (m_buildingLevels[i].level + m_buildingLevels[i].height-base) +
              base;

        return;
    }
}

float CTerrain::GetHardness(const Math::Vector &pos)
{
    float factor = GetBuildingFactor(pos);
    if (factor != 1.0f) return 1.0f;  // on building level

    if (m_materialPoints.empty()) return m_defaultHardness;

    float dim = (m_mosaicCount*m_brickCount*m_brickSize)/2.0f;

    int x, y;

    x = static_cast<int>((pos.x+dim)/m_brickSize);
    y = static_cast<int>((pos.z+dim)/m_brickSize);

    if ( x < 0 || x > m_mosaicCount*m_brickCount ||
         y < 0 || y > m_mosaicCount*m_brickCount )  return m_defaultHardness;

    x /= m_brickCount/m_textureSubdivCount;
    y /= m_brickCount/m_textureSubdivCount;

    if ( x < 0 || x >= m_materialPointCount ||
         y < 0 || y >= m_materialPointCount )  return m_defaultHardness;

    int id = m_materialPoints[x+y*m_materialPointCount].id;
    TerrainMaterial* tm = FindMaterial(id);
    if (tm == nullptr) return m_defaultHardness;

    return tm->hardness;
}

void CTerrain::ShowFlatGround(Math::Vector pos)
{
    static char table[41*41] = { 1 };

    float radius = 3200.0f/1024.0f;

    for (int y = 0; y <= 40; y++)
    {
        for (int x = 0; x <= 40; x++)
        {
            int i = x + y*41;
            table[i] = 0;

            Math::Vector p;
            p.x = (x-20)*radius;
            p.z = (y-20)*radius;
            p.y = 0.0f;

            if (Math::Point(p.x, p.y).Length() > 20.0f*radius)
                continue;

            float angle = GetFineSlope(pos+p);

            if (angle < TERRAIN_FLATLIMIT)
                table[i] = 1;
            else
                table[i] = 2;
        }
    }

    m_engine->CreateGroundMark(pos, 40.0f, 0.001f, 15.0f, 0.001f, 41, 41, table);
}

float CTerrain::GetFlatZoneRadius(Math::Vector center, float max)
{
    float angle = GetFineSlope(center);
    if (angle >= TERRAIN_FLATLIMIT)
        return 0.0f;

    float ref = GetFloorLevel(center, true);
    Math::Point c(center.x, center.z);
    float radius = 1.0f;

    while (radius <= max)
    {
        angle = 0.0f;
        int nb = static_cast<int>(2.0f*Math::PI*radius);
        if (nb < 8) nb = 8;

        Math::Point p (center.x+radius, center.z);
        for (int i = 0; i < nb; i++)
        {
            Math::Point result = Math::RotatePoint(c, angle, p);
            Math::Vector pos;
            pos.x = result.x;
            pos.z = result.y;
            float h = GetFloorLevel(pos, true);
            if ( fabs(h-ref) > 1.0f )  return radius;

            angle += Math::PI*2.0f/8.0f;
        }
        radius += 1.0f;
    }
    return max;
}

void CTerrain::SetFlyingMaxHeight(float height)
{
    m_flyingMaxHeight = height;
}

float CTerrain::GetFlyingMaxHeight()
{
    return m_flyingMaxHeight;
}

void CTerrain::FlushFlyingLimit()
{
    m_flyingMaxHeight = 280.0f;
    m_flyingLimits.clear();
}

void CTerrain::AddFlyingLimit(Math::Vector center,
                                   float extRadius, float intRadius,
                                   float maxHeight)
{
    FlyingLimit fl;
    fl.center    = center;
    fl.extRadius = extRadius;
    fl.intRadius = intRadius;
    fl.maxHeight = maxHeight;
    m_flyingLimits.push_back(fl);
}

float CTerrain::GetFlyingLimit(Math::Vector pos, bool noLimit)
{
    if (noLimit)
        return 280.0f;

    if (m_flyingLimits.empty())
        return m_flyingMaxHeight;

    for (int i = 0; i < static_cast<int>( m_flyingLimits.size() ); i++)
    {
        float dist = Math::DistanceProjected(pos, m_flyingLimits[i].center);

        if (dist >= m_flyingLimits[i].extRadius)
            continue;

        if (dist <= m_flyingLimits[i].intRadius)
            return m_flyingLimits[i].maxHeight;

        dist -= m_flyingLimits[i].intRadius;

        float h = dist * (m_flyingMaxHeight - m_flyingLimits[i].maxHeight) /
                  (m_flyingLimits[i].extRadius - m_flyingLimits[i].intRadius);

        return h + m_flyingLimits[i].maxHeight;
    }

    return m_flyingMaxHeight;
}


} // namespace Gfx
