/*
 * AC3D loader for models generated by the AC3D modeller (www.ac3d.org)
 * part of this source code were supplied by the AC3D project (Andy Colebourne)
 * eg the basic parsing of an AC3D file.
 *
 * Adapted for FlightGear by Fernando García Liñán, 2024.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "ReaderWriterAC3D.hxx"

#include <cstdlib>
#include <iostream>
#include <limits>
#include <vector>

#include <osg/BlendFunc>
#include <osg/CullFace>
#include <osg/Geode>
#include <osg/Geometry>
#include <osg/Group>
#include <osg/Math>
#include <osg/Notify>
#include <osg/StateSet>
#include <osg/Texture2D>

#include <osgUtil/Tessellator>

#include <osgDB/FileNameUtils>
#include <osgDB/Registry>
#include <osgDB/ReadFile>
#include <osgDB/FileUtils>
#include <osgDB/fstream>

namespace ac3d {

enum {
    ObjectTypeNormal = 0,
    ObjectTypeGroup = 1,
    ObjectTypeLight = 2,

    SurfaceTypePolygon = 0,
    SurfaceTypeLineLoop = 1,
    SurfaceTypeLineStrip = 2,

    SurfaceShaded = 1<<4,
    SurfaceTwoSided = 1<<5
};

/// Returns a possibly quoted string given in the end of the current line in the stream
static std::string
readString(std::istream& stream)
{
    std::string s;
    stream >> std::ws;
    if (stream.peek() != '\"') {
        // Not quoted, just read the string
        stream >> s;
    } else {
        // look for quoted strings
        // throw away the quote
        stream.get();
        // extract characters until either an error happens or a quote is found
        while (stream.good()) {
            std::istream::char_type c;
            stream.get(c);
            if (c == '\"')
                break;
            s += c;
        }
    }
    return s;
}

// Just a container to store an ac3d material
class MaterialData {
public:
    MaterialData() :
        mColorArray(new osg::Vec4Array(1)),
        mTranslucent(false)
    {
        mColorArray->setDataVariance(osg::Object::STATIC);
    }

    void readMaterial(std::istream& stream)
    {
        // FlightGear: the underlying osg::Material has been removed, but we
        // still need to consume the tokens on the stream.

        // note that this might be quoted
        std::string name = readString(stream);
        // mMaterial->setName(name);
        std::string tmp;
        stream >> tmp;
        osg::Vec4 diffuse;
        stream >> diffuse[0] >> diffuse[1] >> diffuse[2];
        // mMaterial->setDiffuse(osg::Material::FRONT_AND_BACK, diffuse);
        stream >> tmp;
        osg::Vec4 ambient;
        stream >> ambient[0] >> ambient[1] >> ambient[2];
        // mMaterial->setAmbient(osg::Material::FRONT_AND_BACK, ambient);
        stream >> tmp;
        osg::Vec4 emmissive;
        stream >> emmissive[0] >> emmissive[1] >> emmissive[2];
        // mMaterial->setEmission(osg::Material::FRONT_AND_BACK, emmissive);
        stream >> tmp;
        osg::Vec4 specular;
        stream >> specular[0] >> specular[1] >> specular[2];
        // mMaterial->setSpecular(osg::Material::FRONT_AND_BACK, specular);
        stream >> tmp;
        float shininess;
        stream >> shininess;
        // mMaterial->setShininess(osg::Material::FRONT_AND_BACK, shininess);
        stream >> tmp;
        float transparency;
        stream >> transparency;
        // mMaterial->setTransparency(osg::Material::FRONT_AND_BACK, transparency);
        mTranslucent = 0 < transparency;

        // must correspond to the material we use for the color array below
        // mMaterial->setColorMode(osg::Material::DIFFUSE);
        // this must be done past the transparency setting ...
        (*mColorArray)[0] = diffuse;
    }

    osg::Vec4Array* getColorArray() const
    {
        return mColorArray.get();
    }

    bool isTranslucent() const
    {
        return mTranslucent;
    }

private:
    osg::ref_ptr<osg::Vec4Array> mColorArray;
    bool mTranslucent;
};

class TextureData {
public:
    TextureData() :
        mTranslucent(false),
        mRepeat(true)
    {
    }

    bool setTexture(const std::string& name, const osgDB::ReaderWriter::Options* options)
    {
        mTexture2DRepeat = new osg::Texture2D;
        mTexture2DRepeat->setDataVariance(osg::Object::STATIC);
        mTexture2DRepeat->setWrap(osg::Texture2D::WRAP_S, osg::Texture2D::REPEAT);
        mTexture2DRepeat->setWrap(osg::Texture2D::WRAP_T, osg::Texture2D::REPEAT);

        mTexture2DClamp = new osg::Texture2D;
        mTexture2DClamp->setDataVariance(osg::Object::STATIC);
        mTexture2DClamp->setWrap(osg::Texture2D::WRAP_S, osg::Texture2D::CLAMP_TO_EDGE);
        mTexture2DClamp->setWrap(osg::Texture2D::WRAP_T, osg::Texture2D::CLAMP_TO_EDGE);

        std::string absFileName = osgDB::findDataFile(name, options);
        if (absFileName.empty()) {
            OSG_FATAL << "osgDB ac3d reader: could not find texture \"" << name << "\"" << std::endl;
            return false;
        }

        mImage = osgDB::readRefImageFile(absFileName, options);
        if (!mImage.valid()) {
            OSG_FATAL << "osgDB ac3d reader: could not read texture \"" << name << "\"" << std::endl;
            return false;
        }

        mTexture2DRepeat->setImage(mImage.get());
        mTexture2DClamp->setImage(mImage.get());
        mTranslucent = mImage->isImageTranslucent();

        return true;
    }

    void setRepeat(bool repeat)
    {
        mRepeat = repeat;
    }

    bool valid() const
    {
        return mImage.valid();
    }

    std::string getFileName() const
    {
        if (!mImage.valid())
            return std::string();
        return mImage->getFileName();
    }

    void toTextureStateSet(osg::StateSet* stateSet) const
    {
        if (!valid())
            return;
        if (mRepeat)
            stateSet->setTextureAttribute(0, mTexture2DRepeat.get());
        else
            stateSet->setTextureAttribute(0, mTexture2DClamp.get());
        stateSet->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::ON);
        // if (mTranslucent)
        //     setTranslucent(stateSet);
    }
private:
    osg::ref_ptr<osg::Texture2D> mTexture2DClamp;
    osg::ref_ptr<osg::Texture2D> mTexture2DRepeat;
    osg::ref_ptr<osg::Image> mImage;
    bool mTranslucent;
    bool mRepeat;
};

class FileData {
public:
    FileData(const osgDB::ReaderWriter::Options* options) :
        mOptions(options)
    {
    }

    TextureData toTextureData(const std::string& texName)
    {
        // If it is already there, use this
        TextureDataMap::iterator i = mTextureStates.find(texName);
        if (i != mTextureStates.end())
            return i->second;
        // Try to load that texture.
        TextureData textureData;
        textureData.setTexture(texName, mOptions.get());
        if (textureData.valid()) {
            mTextureStates[texName] = textureData;
            return textureData;
        }
        // still no joy?, try with the stripped filename if this is different
        // Try the pure file name if it is different
        std::string simpleTexName = osgDB::getSimpleFileName(texName);
        if (simpleTexName != texName)
            return toTextureData(simpleTexName);

        // Nothing that worked, return invalid data
        return TextureData();
    }

    void addMaterial(const MaterialData& material)
    {
        mMaterials.push_back(material);
    }

    unsigned getNumMaterials() const
    {
        return mMaterials.size();
    }

    const MaterialData& getMaterial(unsigned idx) const
    {
        return mMaterials[idx];
    }

private:
    /// Stores the ac3d file reader options, only used for reading texture files
    osg::ref_ptr<osgDB::ReaderWriter::Options const> mOptions;

    /// The list of ac3d MATERIALS
    std::vector<MaterialData> mMaterials;

    /// Local per model texture attribute cache.
    /// ... images are usually cached in the registries object cache
    typedef std::map<std::string, TextureData> TextureDataMap;
    TextureDataMap mTextureStates;
};

struct RefData {
    RefData(const osg::Vec3& _weightedNormal, const osg::Vec2& _texCoord, bool _smooth) :
        weightedFlatNormal(_weightedNormal),
        weightedFlatNormalLength(_weightedNormal.length()),
        texCoord(_texCoord),
        smooth(_smooth)
    { }
    // weighted flat surface normal
    osg::Vec3 weightedFlatNormal;
    float weightedFlatNormalLength;
    osg::Vec2 texCoord;
    // resulting vertex normal
    osg::Vec3 finalNormal;
    // if zero no need to smooth
    unsigned smooth;
};

struct VertexData {
    VertexData(const osg::Vec3& vertex) : _vertex(vertex) {}
    unsigned addRefData(const RefData& refData)
    {
        unsigned index = _refs.size();
        _refs.push_back(refData);
        return index;
    }

    void collect(float cosCreaseAngle, const RefData& ref)
    {
        unsigned size = _refs.size();
        for (unsigned i = 0; i < size; ++i)
        {
            if (_refs[i].smooth == ~0u)
            {
                float dot = _refs[i].weightedFlatNormal*ref.weightedFlatNormal;
                float lengths = _refs[i].weightedFlatNormalLength*ref.weightedFlatNormalLength;
                if (cosCreaseAngle*lengths <= dot)
                {
                    // Ok put that into the current set
                    _refs[i].smooth = ref.smooth;
                    collect(cosCreaseAngle, _refs[i]);
                }
            }
        }
    }

    void smoothNormals(float cosCreaseAngle)
    {
        // compute sets of vertices smoothed to the same normal
        // if smooth is zero we do not need to smooth
        // in a first pass mark all refs not yet in a set to ~0u
        unsigned size = _refs.size();
        for (unsigned i = 0; i < size; ++i)
        {
            if (_refs[i].smooth)
            {
                _refs[i].smooth = ~0u;
            }
        }
        // Now collect the sets
        unsigned currentSet = 1;
        for (unsigned i = 0; i < size; ++i)
        {
            if (_refs[i].smooth == ~0u)
            {
                _refs[i].smooth = currentSet++;
                collect(cosCreaseAngle, _refs[i]);
            }
        }
        // smooth and normalize the sets
        for (--currentSet; 0 < currentSet; --currentSet)
        {
            osg::Vec3 normal(0, 0, 0);
            for (unsigned i = 0; i < size; ++i)
            {
                if (_refs[i].smooth == currentSet)
                {
                    normal += _refs[i].weightedFlatNormal;
                }
            }
            normal.normalize();
            for (unsigned i = 0; i < size; ++i)
            {
                if (_refs[i].smooth == currentSet)
                {
                    _refs[i].finalNormal = normal;
                }
            }
        }

        // normalize the ones which do not need smoothing
        for (unsigned i = 0; i < size; ++i)
        {
            if (_refs[i].smooth == 0)
            {
                _refs[i].finalNormal = _refs[i].weightedFlatNormal;
                _refs[i].finalNormal.normalize();
            }
        }
    }
    osg::Vec3 _vertex;
    std::vector<RefData> _refs;
};
struct VertexIndex {
    VertexIndex(unsigned _vertexIndex = 0, unsigned _refIndex = 0) :
        vertexIndex(_vertexIndex), refIndex(_refIndex)
    { }
    unsigned vertexIndex;
    unsigned refIndex;
};

class VertexSet : public osg::Referenced {
public:
    VertexSet() :
        _cosCreaseAngle(1.0f),
        _dirty(true)
    { }
    void reserve(unsigned n)
    {
        _vertices.reserve(n);
    }
    unsigned size() const
    {
        return _vertices.size();
    }
    void setCreaseAngle(float crease)
    {
        _dirty = true;
        if (crease <= 0)
            _cosCreaseAngle = 1.0f;
        else if (180 <= crease)
            _cosCreaseAngle = -1.0f;
        else
            _cosCreaseAngle = cosf(osg::DegreesToRadians(crease));
    }
    void addVertex(const osg::Vec3& vertex)
    {
        _dirty = true;
        _vertices.push_back(vertex);
    }
    const osg::Vec3& getVertex(unsigned index)
    {
        return _vertices[index]._vertex;
    }
    const osg::Vec3& getVertex(const VertexIndex& vertexIndex)
    {
        return _vertices[vertexIndex.vertexIndex]._vertex;
    }
    const osg::Vec3& getNormal(const VertexIndex& vertexIndex)
    {
        if (_dirty)
            smoothNormals();
        return _vertices[vertexIndex.vertexIndex]._refs[vertexIndex.refIndex].finalNormal;
    }
    const osg::Vec2& getTexCoord(const VertexIndex& vertexIndex)
    {
        return _vertices[vertexIndex.vertexIndex]._refs[vertexIndex.refIndex].texCoord;
    }

    VertexIndex addRefData(unsigned i, const RefData& refData)
    {
        if (_vertices.size() <= i)
        {
            OSG_FATAL << "osgDB ac3d reader: internal error, got invalid vertex index!" << std::endl;
            return VertexIndex(0, 0);
        }
        _dirty = true;
        return VertexIndex(i, _vertices[i].addRefData(refData));
    }

private:
    void smoothNormals()
    {
        std::vector<VertexData>::iterator i;
        for (i = _vertices.begin(); i != _vertices.end(); ++i)
        {
            i->smoothNormals(_cosCreaseAngle);
        }
        _dirty = false;
    }

    std::vector<VertexData> _vertices;
    float _cosCreaseAngle;
    bool _dirty;
};


class PrimitiveBin : public osg::Referenced {
public:
    PrimitiveBin(unsigned flags, VertexSet* vertexSet) :
        _geode(new osg::Geode),
        _vertexSet(vertexSet),
        _flags(flags)
    {
        _geode->setDataVariance(osg::Object::STATIC);
    }

    virtual bool beginPrimitive(unsigned nRefs) = 0;
    virtual bool vertex(unsigned vertexIndex, const osg::Vec2& texCoord) = 0;
    virtual bool endPrimitive() = 0;

    virtual osg::Geode* finalize(const MaterialData& material, const TextureData& textureData) = 0;

protected:
    bool isLineLoop() const
    {
        return (_flags & SurfaceTypeLineLoop)!=0;
    }
    bool isLineStrip() const
    {
        return (_flags & SurfaceTypeLineStrip)!=0;
    }
    bool isTwoSided() const
    {
        return (_flags & SurfaceTwoSided)!=0;
    }
    bool isSmooth() const
    {
        return (_flags & SurfaceShaded)!=0;
    }

    osg::ref_ptr<osg::Geode> _geode;
    osg::ref_ptr<VertexSet> _vertexSet;

private:
    unsigned _flags;
};

class LineBin : public PrimitiveBin {
private:
    osg::ref_ptr<osg::Geometry> _geometry;
    osg::ref_ptr<osg::Vec3Array> _vertices;
    osg::ref_ptr<osg::Vec2Array> _texCoords;
    struct Ref {
        osg::Vec2 texCoord;
        unsigned index;
    };
    std::vector<Ref> _refs;

public:
    LineBin(unsigned flags, VertexSet* vertexSet) :
        PrimitiveBin(flags, vertexSet),
        _geometry(new osg::Geometry),
        _vertices(new osg::Vec3Array),
        _texCoords(new osg::Vec2Array)
    {
        _geometry->setDataVariance(osg::Object::STATIC);
        _vertices->setDataVariance(osg::Object::STATIC);
        _texCoords->setDataVariance(osg::Object::STATIC);
        _geometry->setVertexArray(_vertices.get());
        _geometry->setTexCoordArray(0, _texCoords.get());
    }

    virtual bool beginPrimitive(unsigned nRefs)
    {
        // Check if we have enough for a line or something broken ...
        if (nRefs < 2) {
            OSG_WARN << "osgDB ac3d reader: detected line with less than 2 vertices!" << std::endl;
            return false;
        }

        _refs.reserve(nRefs);
        _refs.resize(0);
        return true;
    }
    virtual bool vertex(unsigned vertexIndex, const osg::Vec2& texCoord)
    {
        Ref ref;
        ref.index = vertexIndex;
        ref.texCoord = texCoord;
        _refs.push_back(ref);
        return true;
    }
    virtual bool endPrimitive()
    {
        GLint type;
        if (isLineLoop())
            type = osg::PrimitiveSet::LINE_LOOP;
        else if (isLineStrip())
            type = osg::PrimitiveSet::LINE_STRIP;
        else {
            OSG_FATAL << "osgDB ac3d reader: non surface flags in surface bin!" << std::endl;
            return false;
        }
        unsigned nRefs = _refs.size();
        unsigned start = _vertices->size();
        for (unsigned i = 0; i < nRefs; ++i) {
            osg::Vec3 vertex = _vertexSet->getVertex(_refs[i].index);
            _vertices->push_back(vertex);
            _texCoords->push_back(_refs[i].texCoord);
        }
        _geometry->addPrimitiveSet(new osg::DrawArrays(type, start, nRefs));

        return true;
    }

    virtual osg::Geode* finalize(const MaterialData& material, const TextureData& /*textureData*/)
    {
        _geode->addDrawable(_geometry.get());
        // material.toStateSet(_geode->getOrCreateStateSet());
        _geometry->setColorArray(material.getColorArray(), osg::Array::BIND_OVERALL);
        _geometry->setNormalArray(0);
        return _geode.get();
    }
};

class SurfaceBin : public PrimitiveBin {
private:
    struct Ref {
        osg::Vec2 texCoord;
        unsigned index;
    };
    std::vector<Ref> _refs;

    struct TriangleData {
        VertexIndex index[3];
    };
    std::vector<TriangleData> _triangles;

    struct QuadData {
        VertexIndex index[4];
    };
    std::vector<QuadData> _quads;

    struct PolygonData {
        std::vector<VertexIndex> index;
    };
    std::vector<PolygonData> _polygons;
    std::vector<PolygonData> _toTessellatePolygons;

    typedef std::pair<osg::Vec3, osg::Vec3> VertexNormalPair;
    typedef std::pair<VertexNormalPair, osg::Vec2> VertexNormalTexTuple;
    typedef std::map<VertexNormalTexTuple, unsigned> VertexIndexMap;
    VertexIndexMap _vertexIndexMap;

public:
    SurfaceBin(unsigned flags, VertexSet *vertexSet) :
        PrimitiveBin(flags, vertexSet)
    { }

    virtual bool beginPrimitive(unsigned nRefs)
    {
        _refs.reserve(nRefs);
        _refs.clear();

        // Check if we have enough for a line or something broken ...
        if (nRefs < 3) {
            OSG_WARN << "osgDB ac3d reader: detected surface with less than 3 vertices!" << std::endl;
            return false;
        }
        return true;
    }
    virtual bool vertex(unsigned vertexIndex, const osg::Vec2& texCoord)
    {
        Ref ref;
        ref.index = vertexIndex;
        ref.texCoord = texCoord;
        _refs.push_back(ref);
        return true;
    }
    virtual bool endPrimitive()
    {
        unsigned nRefs = _refs.size();

        // Compute the normal times the enclosed area.
        // During that check if the surface is convex. If so, put in the surface as such.
        bool needTessellation = false;
        osg::Vec3 prevEdgeNormal;
        osg::Vec3 weightedNormal(0, 0, 0);
        osg::Vec3 v0 = _vertexSet->getVertex(_refs[0].index);
        for (unsigned i = 2; i < nRefs; ++i) {
            osg::Vec3 side1 = _vertexSet->getVertex(_refs[i-1].index) - v0;
            osg::Vec3 side2 = _vertexSet->getVertex(_refs[i].index) - v0;
            osg::Vec3 newNormal = side1^side2;
            if (!needTessellation) {
                if (3 < nRefs && newNormal*weightedNormal < 0) {
                    needTessellation = true;
                }
                if (i < 3) {
                    prevEdgeNormal = newNormal;
                } else { // if (3 <= i) // due to the for loop
                    osg::Vec3 sideim1 = _vertexSet->getVertex(_refs[i-1].index) - _vertexSet->getVertex(_refs[i-2].index);
                    osg::Vec3 sidei = _vertexSet->getVertex(_refs[i].index) - _vertexSet->getVertex(_refs[i-2].index);
                    osg::Vec3 edgeNormal = sideim1^sidei;
                    if (edgeNormal*prevEdgeNormal < 0) {
                        needTessellation = true;
                    }
                    prevEdgeNormal = edgeNormal;
                }
            }

            weightedNormal += newNormal;
        }

        if (needTessellation) {
            unsigned polygonIndex = _toTessellatePolygons.size();
            _toTessellatePolygons.resize(polygonIndex + 1);
            for (unsigned i = 0; i < nRefs; ++i) {
                RefData refData(weightedNormal, _refs[i].texCoord, isSmooth());
                VertexIndex vertexIndex = _vertexSet->addRefData(_refs[i].index, refData);
                _toTessellatePolygons[polygonIndex].index.push_back(vertexIndex);
            }
        } else if (nRefs == 3) {
            unsigned triangleIndex = _triangles.size();
            _triangles.resize(triangleIndex + 1);
            for (unsigned i = 0; i < 3; ++i) {
                RefData refData(weightedNormal, _refs[i].texCoord, isSmooth());
                VertexIndex vertexIndex = _vertexSet->addRefData(_refs[i].index, refData);
                _triangles[triangleIndex].index[i] = vertexIndex;
            }
        } else if (nRefs == 4) {
            unsigned quadIndex = _quads.size();
            _quads.resize(quadIndex + 1);
            for (unsigned i = 0; i < 4; ++i) {
                RefData refData(weightedNormal, _refs[i].texCoord, isSmooth());
                VertexIndex vertexIndex = _vertexSet->addRefData(_refs[i].index, refData);
                _quads[quadIndex].index[i] = vertexIndex;
            }
        } else {
            unsigned polygonIndex = _polygons.size();
            _polygons.resize(polygonIndex + 1);
            for (unsigned i = 0; i < nRefs; ++i) {
                RefData refData(weightedNormal, _refs[i].texCoord, isSmooth());
                VertexIndex vertexIndex = _vertexSet->addRefData(_refs[i].index, refData);
                _polygons[polygonIndex].index.push_back(vertexIndex);
            }
        }
        return true;
    }

    unsigned pushVertex(const VertexIndex& vertexIndex, osg::Vec3Array* vertexArray,
                        osg::Vec3Array* normalArray, osg::Vec2Array* texcoordArray)
    {
        VertexNormalTexTuple vertexNormalTexTuple;
        vertexNormalTexTuple.first.first = _vertexSet->getVertex(vertexIndex);
        vertexNormalTexTuple.first.second = _vertexSet->getNormal(vertexIndex);
        if (texcoordArray)
            vertexNormalTexTuple.second = _vertexSet->getTexCoord(vertexIndex);
        else
            vertexNormalTexTuple.second = osg::Vec2(0, 0);

        VertexIndexMap::iterator i = _vertexIndexMap.find(vertexNormalTexTuple);
        if (i != _vertexIndexMap.end())
            return i->second;

        unsigned index = vertexArray->size();
        vertexArray->push_back(vertexNormalTexTuple.first.first);
        normalArray->push_back(vertexNormalTexTuple.first.second);
        if (texcoordArray)
            texcoordArray->push_back(vertexNormalTexTuple.second);

        _vertexIndexMap.insert(VertexIndexMap::value_type(vertexNormalTexTuple, index));

        return index;
    }

    osg::DrawElements* createOptimalDrawElements(osg::DrawElementsUInt* drawElements)
    {
        unsigned num = drawElements->getNumIndices();
        unsigned maxIndex = 0;
        for (unsigned i = 0; i < num; ++i) {
            maxIndex = osg::maximum(maxIndex, drawElements->getElement(i));
        }
        if (maxIndex <= std::numeric_limits<unsigned char>::max()) {
            osg::DrawElementsUByte* drawElementsUByte = new osg::DrawElementsUByte(drawElements->getMode());
            drawElementsUByte->reserveElements(num);
            for (unsigned i = 0; i < num; ++i) {
                drawElementsUByte->addElement(drawElements->getElement(i));
            }
            return drawElementsUByte;
        } else if (maxIndex <= std::numeric_limits<unsigned short>::max()) {
            osg::DrawElementsUShort* drawElementsUShort = new osg::DrawElementsUShort(drawElements->getMode());
            drawElementsUShort->reserveElements(num);
            for (unsigned i = 0; i < num; ++i) {
                drawElementsUShort->addElement(drawElements->getElement(i));
            }
            return drawElementsUShort;
        } else {
            return drawElements;
        }
    }

    virtual osg::Geode* finalize(const MaterialData& material, const TextureData& textureData)
    {
        osg::StateSet* stateSet = _geode->getOrCreateStateSet();
        // material.toStateSet(stateSet);
        textureData.toTextureStateSet(stateSet);

        // Single- or doublesided culling
        if (isTwoSided()) {
            stateSet->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
        } else {
            osg::CullFace* cullFace = new osg::CullFace;
            cullFace->setDataVariance(osg::Object::STATIC);
            cullFace->setMode(osg::CullFace::BACK);
            stateSet->setAttribute(cullFace);
            stateSet->setMode(GL_CULL_FACE, osg::StateAttribute::ON);
        }

        // Set up the arrays, always store texture coords, may be we need them later ...
        osg::Geometry* geometry = new osg::Geometry;
        _geode->addDrawable(geometry);
        geometry->setDataVariance(osg::Object::STATIC);
        geometry->setUseVertexBufferObjects(true);
        geometry->setColorArray(material.getColorArray(), osg::Array::BIND_OVERALL);
        osg::Vec3Array* normalArray = new osg::Vec3Array;
        geometry->setNormalArray(normalArray, osg::Array::BIND_PER_VERTEX);
        osg::Vec3Array* vertexArray = new osg::Vec3Array;
        geometry->setVertexArray(vertexArray);
        osg::Vec2Array* texcoordArray = 0;
        if (textureData.valid()) {
            texcoordArray = new osg::Vec2Array;
            geometry->setTexCoordArray(0, texcoordArray);
        }

        // At first handle the polygons to tessellate, fix them and append the other polygons later
        if (!_toTessellatePolygons.empty()) {
            for (unsigned i = 0; i < _toTessellatePolygons.size(); ++i) {
                osg::ref_ptr<osg::DrawElementsUInt> drawElements = new osg::DrawElementsUInt(osg::PrimitiveSet::POLYGON);
                for (unsigned j = 0; j < _toTessellatePolygons[i].index.size(); ++j) {
                    unsigned index = pushVertex(_toTessellatePolygons[i].index[j], vertexArray, normalArray, texcoordArray);
                    drawElements->addElement(index);
                }
                geometry->addPrimitiveSet(createOptimalDrawElements(drawElements.get()));
            }

            // Hopefully the result of the tessellator is triangles. If they
            // output quads or polygons, we won't be able to render them.
            osgUtil::Tessellator Tessellator;
            Tessellator.retessellatePolygons(*geometry);
        }

        osg::ref_ptr<osg::DrawElementsUInt> drawElements = new osg::DrawElementsUInt(osg::PrimitiveSet::TRIANGLES);

        // handle triangles
        if (!_triangles.empty()) {
            for (size_t i = 0; i < _triangles.size(); ++i) {
                for (size_t j = 0; j < 3; ++j) {
                    unsigned index = pushVertex(_triangles[i].index[j], vertexArray, normalArray, texcoordArray);
                    drawElements->addElement(index);
                }
            }
        }

        // handle quads as 2 triangles (GL_QUADS is not available anymore)
        if (!_quads.empty()) {
            for (size_t i = 0; i < _quads.size(); ++i) {
                unsigned indices[4];
                for (size_t j = 0; j < 4; ++j) {
                    indices[j] = pushVertex(_quads[i].index[j], vertexArray, normalArray, texcoordArray);
                }
                // First triangle
                drawElements->addElement(indices[0]);
                drawElements->addElement(indices[1]);
                drawElements->addElement(indices[2]);
                // Second triangle
                drawElements->addElement(indices[0]);
                drawElements->addElement(indices[2]);
                drawElements->addElement(indices[3]);
            }
        }

        // handle polygons (assuming they are convex)
        // For an n-sided polygon, create n-2 triangles using fan triangulation
        if (!_polygons.empty()) {
            for (size_t i = 0; i < _polygons.size(); ++i) {
                size_t polygon_vertex_count = _polygons[i].index.size();
                std::vector<unsigned> indices;
                indices.reserve(polygon_vertex_count);
                for (size_t j = 0; j < polygon_vertex_count; ++j) {
                    indices.push_back(pushVertex(_polygons[i].index[j], vertexArray, normalArray, texcoordArray));
                }
                for (size_t j = 2; j < polygon_vertex_count; ++j) {
                    drawElements->addElement(indices[0]);
                    drawElements->addElement(indices[j-1]);
                    drawElements->addElement(indices[j]);
                }
            }
        }

        geometry->addPrimitiveSet(createOptimalDrawElements(drawElements.get()));

        return _geode.get();
    }
};

struct Bins {
    PrimitiveBin* getOrCreatePrimitiveBin(unsigned flags, VertexSet* vertexSet)
    {
        if ((flags & SurfaceTypeLineLoop) || (flags & SurfaceTypeLineStrip)) {
            if (!lineBin.valid()) {
                lineBin = new LineBin(flags, vertexSet);
            }
            return lineBin.get();
        } else if (flags & SurfaceShaded) {
            if (flags & SurfaceTwoSided) {
                if (!smoothDoubleSurfaceBin.valid()) {
                    smoothDoubleSurfaceBin = new SurfaceBin(flags, vertexSet);
                }
                return smoothDoubleSurfaceBin.get();
            } else {
                if (!smoothSingleSurfaceBin.valid()) {
                    smoothSingleSurfaceBin = new SurfaceBin(flags, vertexSet);
                }
                return smoothSingleSurfaceBin.get();
            }
        } else {
            if (flags & SurfaceTwoSided) {
                if (!flatDoubleSurfaceBin.valid()) {
                    flatDoubleSurfaceBin = new SurfaceBin(flags, vertexSet);
                }
                return flatDoubleSurfaceBin.get();
            } else {
                if (!flatSingleSurfaceBin.valid()) {
                    flatSingleSurfaceBin = new SurfaceBin(flags, vertexSet);
                }
                return flatSingleSurfaceBin.get();
            }
        }
    }

    void finalize(osg::Group* group, const MaterialData& material, const TextureData& textureData)
    {
        if (lineBin.valid()) {
            group->addChild(lineBin->finalize(material, textureData));
        }
        if (smoothDoubleSurfaceBin.valid()) {
            group->addChild(smoothDoubleSurfaceBin->finalize(material, textureData));
        }
        if (smoothSingleSurfaceBin.valid()) {
            group->addChild(smoothSingleSurfaceBin->finalize(material, textureData));
        }
        if (flatDoubleSurfaceBin.valid()) {
            group->addChild(flatDoubleSurfaceBin->finalize(material, textureData));
        }
        if (flatSingleSurfaceBin.valid()) {
            group->addChild(flatSingleSurfaceBin->finalize(material, textureData));
        }
    }

private:
    osg::ref_ptr<LineBin> lineBin;
    osg::ref_ptr<SurfaceBin> flatDoubleSurfaceBin;
    osg::ref_ptr<SurfaceBin> flatSingleSurfaceBin;
    osg::ref_ptr<SurfaceBin> smoothDoubleSurfaceBin;
    osg::ref_ptr<SurfaceBin> smoothSingleSurfaceBin;
};

osg::Node*
readObject(std::istream& stream, FileData& fileData, const osg::Matrix& parentTransform, TextureData textureData)
{
    // most of this logic came from Andy Colebourne (developer of the AC3D editor) so it had better be right!

    // The transform configured in this current object level
    osg::Matrix transform;
    // The vertex pool in this object
    osg::ref_ptr<VertexSet> vertexSet = new VertexSet;
    osg::ref_ptr<osg::Group> group = new osg::Group;
    group->setDataVariance(osg::Object::STATIC);
    osg::Vec2 textureOffset(0, 0);
    osg::Vec2 textureRepeat(1, 1);
    float creaseAngle = 61;
    unsigned objectType = ObjectTypeGroup;

    while (!stream.eof() && stream.good()) {
        std::string token;
        stream >> token;

        if (token == "MATERIAL") {
            MaterialData mat;
            mat.readMaterial(stream);
            fileData.addMaterial(mat);
        }
        else if (token == "OBJECT") {
            std::string type;
            stream >> type;

            if (type == "group")
                objectType = ObjectTypeGroup;
            else if (type == "light")
                objectType = ObjectTypeLight;
            else if (type == "world")
                objectType = ObjectTypeGroup;
            else
                objectType = ObjectTypeNormal;
        }
        else if (token == "crease") {
            stream >> creaseAngle;
        }
        else if (token == "data") {
            int len;
            stream >> len;
            std::vector<char> tmp(len);
            stream.read(&(tmp[0]), len);
        }
        else if (token == "name") {
            group->setName(readString(stream));
        }
        else if (token == "texture") {
            // read the texture name
            textureData = fileData.toTextureData(readString(stream));
        }
        else if (token == "texrep") {
            stream >> textureRepeat[0] >> textureRepeat[1];
//             if (textureRepeat[0] == 0.0f && textureRepeat[1] == 0.0f)
//                 textureData.setRepeat(false);
//             else
//                 textureData.setRepeat(true);
        }
        else if (token == "texoff") {
            stream >> textureOffset[0] >> textureOffset[1];
        }
        else if (token == "rot") {
            for (unsigned n = 0; n < 3; ++n)
                for (unsigned m = 0; m < 3; ++m)
#if 1
                    stream >> transform(n, m);
#else
            stream >> transform(m, n);
#endif
        }
        else if (token == "loc") {
            for (unsigned n = 0; n < 3; ++n)
                stream >> transform(3, n);
        }
        else if (token == "url") {
            std::string url;
            stream >> url;
            group->addDescription(url);
        }
        else if (token == "numvert") {
            osg::Matrix currentTransform = transform*parentTransform;

            unsigned num;
            stream >> num;
            if (num != 0) {
                vertexSet->reserve(num);

                for (unsigned n = 0; n < num; ++n) {
                    osg::Vec3 p;
                    stream >> p[0] >> p[1] >> p[2];
                    vertexSet->addVertex(currentTransform.preMult(p));
                }
            }
        }
        else if (token == "numsurf") {
            unsigned num;
            stream >> num;
            if (0 < num) {
                // list of materials required- generate one geode per material
                std::vector<Bins> primitiveBins(fileData.getNumMaterials());
                vertexSet->setCreaseAngle(creaseAngle);

                for (unsigned n = 0; n < num; ++n) {
                    std::string inner_token;
                    stream >> inner_token;

                    if (inner_token != "SURF") {
                        OSG_FATAL << "osgDB ac3d reader: expected SURF line while reading object \""
                                  << group->getName() << "\"!" << std::endl;
                        return group.release();
                    }

                    stream >> inner_token;
                    unsigned flags = strtol(inner_token.c_str(), NULL, 0);

                    stream >> inner_token;
                    if (inner_token != "mat") {
                        OSG_FATAL << "osgDB ac3d reader: expected mat line while reading object \""
                                  << group->getName() << "\"!" << std::endl;
                        return group.release();
                    }

                    // read the material index
                    unsigned matIdx;
                    stream >> matIdx;
                    if (primitiveBins.size() <= matIdx) {
                        OSG_FATAL << "osgDB ac3d reader: invalid material number while reading object \""
                                  << group->getName() << "\"" << std::endl;
                        return group.release();
                    }

                    // now get the correct PrimitiveBin
                    PrimitiveBin* primitiveBin = 0;
                    primitiveBin = primitiveBins[matIdx].getOrCreatePrimitiveBin(flags, vertexSet.get());
                    if (!primitiveBin) {
                        OSG_FATAL << "osgDB ac3d reader: unexpected primitive flags while reading object \""
                                  << group->getName() << "\"" << std::endl;
                        return group.release();
                    }

                    // read the refs
                    stream >> inner_token;
                    if (inner_token != "refs") {
                        OSG_FATAL << "osgDB ac3d reader: expected refs line while reading object \""
                                  << group->getName() << "\"" << std::endl;
                        return group.release();
                    }

                    unsigned nRefs = 0;
                    stream >> nRefs;
                    if (!stream) {
                        OSG_FATAL << "osgDB ac3d reader: could not read number of refs while reading object \""
                                  << group->getName() << "\"" << std::endl;
                        return group.release();
                    }

                    // in case this is an invalid refs count for this primitive
                    // read further, but do not store that primitive
                    bool acceptPrimitive = primitiveBin->beginPrimitive(nRefs);
                    for (unsigned i = 0; i < nRefs; ++i) {
                        // Read the vertex index
                        unsigned index;
                        stream >> index;
                        if (vertexSet->size() <= index)
                        {
                            OSG_FATAL << "osgDB ac3d reader: invalid ref vertex index while reading object \""
                                      << group->getName() << "\"" << std::endl;
                            return group.release();
                        }

                        // Read the texture coordinates
                        osg::Vec2 texCoord;
                        stream >> texCoord[0] >> texCoord[1];
                        if (!stream) {
                            OSG_WARN << "osgDB ac3d reader: could not parse texture coords while reading object \""
                                     << group->getName() << "\" setting to (0,0)" << std::endl;
                            stream.clear();
                            std::string dummy;
                            std::getline(stream, dummy);
                        }

                        if (acceptPrimitive)
                        {
                            texCoord[0] = textureOffset[0] + texCoord[0]*textureRepeat[0];
                            texCoord[1] = textureOffset[1] + texCoord[1]*textureRepeat[1];

                            if (!primitiveBin->vertex(index, texCoord))
                            {
                                return group.release();
                            }
                        }
                    }
                    if (acceptPrimitive)
                    {
                        if (!primitiveBin->endPrimitive())
                        {
                            return group.release();
                        }
                    }
                }

                for (unsigned i = 0; i < primitiveBins.size(); ++i)
                    primitiveBins[i].finalize(group.get(), fileData.getMaterial(i), textureData);
            }
        }
        else if (token == "kids") {
            unsigned num;
            stream >> num;
            if (num != 0) {
                for (unsigned n = 0; n < num; n++) {
                    osg::Node *k = readObject(stream, fileData, transform*parentTransform, textureData);
                    if (k == 0) {
                        OSG_FATAL << "osgDB ac3d reader: error reading child object" << std::endl;
                        return group.release();
                    }
                    else {
                        group->addChild(k);
                    }
                }
            } else if (objectType == ObjectTypeLight) {
                OSG_WARN << "osgDB ac3d reader: light objects are not supported" << std::endl;
                return group.release();
            }
            return group.release();
        }
    }

    return group.release();
}

osg::Node*
readFile(std::istream& stream, const osgDB::ReaderWriter::Options* options)
{
    FileData fileData(options);
    osg::Matrix identityTransform;
    osg::Node* node = readObject(stream, fileData, identityTransform, TextureData());
    if (node)
        node->setName("World");
    return node;
}

} // namespace ac3d


namespace simgear {

ReaderWriterAC3D::ReaderWriterAC3D()
{
    supportsExtension("ac", "AC3D Database format");
}

ReaderWriterAC3D::~ReaderWriterAC3D()
{
}

ReaderWriterAC3D::ReadResult
ReaderWriterAC3D::readObject(const std::string& location, const Options* options) const
{
    return readNode(location, options);
}


ReaderWriterAC3D::ReadResult
ReaderWriterAC3D::readNode(const std::string& location, const Options* options) const
{
    std::string ext = osgDB::getFileExtension(location);
    if (!acceptsExtension(ext)) return ReadResult::FILE_NOT_HANDLED;

    // GWM added Dec 2003 - get full path name (change in osgDB handling of files).
    std::string filename = osgDB::findDataFile( location, options );
    OSG_INFO << "osgDB ac3d reader: starting reading \"" << filename << "\"" << std::endl;

    // Anders Backmann - correct return if path not found
    if (filename.empty()) return ReadResult::FILE_NOT_FOUND;

    // allocate per file data and start reading
    osgDB::ifstream fin;
    fin.open(filename.c_str(), std::ios::in);
    if (!fin.is_open()) return ReadResult::FILE_NOT_FOUND;

    // code for setting up the database path so that internally referenced file are
    // searched for on relative paths.
    osg::ref_ptr<Options> local_opt;
    if (options)
        local_opt = static_cast<Options*>(options->clone(osg::CopyOp::DEEP_COPY_ALL));
    else
        local_opt = new Options;
    local_opt->getDatabasePathList().push_back(osgDB::getFilePath(filename));

    ReadResult result = readNode(fin, local_opt.get());
    if (result.validNode())
        result.getNode()->setName(filename);
    return result;
}

ReaderWriterAC3D::ReadResult
ReaderWriterAC3D::readObject(std::istream& fin, const Options* options) const
{
    return readNode(fin, options);
}

ReaderWriterAC3D::ReadResult
ReaderWriterAC3D::readNode(std::istream& fin, const Options* options) const
{
    std::string header;
    fin >> header;
    if (header.substr(0, 4) != "AC3D")
        return osgDB::ReaderWriter::ReadResult::FILE_NOT_HANDLED;

    return ac3d::readFile(fin, options);
}

// NOTE: The ModelRegistry callback is defined in ModelRegistry.cxx

// Register the ReaderWriter
REGISTER_OSGPLUGIN(ac, ReaderWriterAC3D)

} // namespace simgear
