/******************************************************************************
* Copyright (c) 2015, Peter J. Gadomski <pete.gadomski@gmail.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following
* conditions are met:
*
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in
*       the documentation and/or other materials provided
*       with the distribution.
*     * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the
*       names of its contributors may be used to endorse or promote
*       products derived from this software without specific prior
*       written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
****************************************************************************/

#include "PlyReader.hpp"

#include <sstream>

#include <pdal/PDALUtils.hpp>
#include <pdal/PointView.hpp>
#include <pdal/util/IStream.hpp>

namespace pdal
{

static StaticPluginInfo const s_info
{
        "readers.ply",
        "Read ply files.",
        "https://pdal.org/stages/reader.ply.html",
        { "ply" }
};

CREATE_STATIC_STAGE(PlyReader, s_info)


PlyReader::PlyReader() : m_vertexElt(nullptr)
{}


std::string PlyReader::readLine()
{
    m_line.clear();
    if (m_lines.size())
    {
        m_line = m_lines.top();
        m_lines.pop();
    }
    else
    {
        do
        {
            std::getline(*m_stream, m_line);
        } while (m_line.empty() && m_stream->good());
    }
    Utils::trimTrailing(m_line);
    m_linePos = Utils::extract(m_line, 0,
        [](char c){ return !std::isspace(c); });
    return std::string(m_line, 0, m_linePos);
}


void PlyReader::pushLine()
{
    m_lines.push(m_line);
}


std::string PlyReader::nextWord()
{
    std::string s;
    std::string::size_type cnt = Utils::extractSpaces(m_line, m_linePos);
    m_linePos += cnt;
    if (m_linePos == m_line.size())
        return s;

    cnt = Utils::extract(m_line, m_linePos,
        [](char c){ return !std::isspace(c); });
    s = std::string(m_line, m_linePos, cnt);
    m_linePos += cnt;
    return s;
}


void PlyReader::extractMagic()
{
    std::string first = readLine();
    if (first != "ply")
        throwError("File isn't a PLY file.  'ply' not found.");
    if (m_linePos != m_line.size())
        throwError("Text found following 'ply' keyword.");
}


void PlyReader::extractEnd()
{
    std::string first = readLine();
    if (first != "end_header")
        throwError("'end_header' expected but found line beginning with '" +
            first + "' instead.");
    if (m_linePos != m_line.size())
        throwError("Text found following 'end_header' keyword.");
}


void PlyReader::extractFormat()
{
    std::string word = readLine();
    if (word != "format")
        throwError("Expected format line not found in PLY file.");

    word = nextWord();
    if (word == "ascii")
        m_format = Format::Ascii;
    else if (word == "binary_big_endian")
        m_format = Format::BinaryBe;
    else if (word == "binary_little_endian")
        m_format = Format::BinaryLe;
    else
        throwError("Unrecognized PLY format: '" + word + "'.");

    word = nextWord();
    if (word != "1.0")
        throwError("Unsupported PLY version: '" + word + "'.");
}


Dimension::Type PlyReader::getType(const std::string& name)
{
    static std::map<std::string, Dimension::Type> types =
    {
        { "int8", Dimension::Type::Signed8 },
        { "uint8", Dimension::Type::Unsigned8 },
        { "int16", Dimension::Type::Signed16 },
        { "uint16", Dimension::Type::Unsigned16 },
        { "int32", Dimension::Type::Signed32 },
        { "uint32", Dimension::Type::Unsigned32 },
        { "float32", Dimension::Type::Float },
        { "float64", Dimension::Type::Double },

        { "char", Dimension::Type::Signed8 },
        { "uchar", Dimension::Type::Unsigned8 },
        { "short", Dimension::Type::Signed16 },
        { "ushort", Dimension::Type::Unsigned16 },
        { "int", Dimension::Type::Signed32 },
        { "uint", Dimension::Type::Unsigned32 },
        { "float", Dimension::Type::Float },
        { "double", Dimension::Type::Double }
    };

    try
    {
        return types.at(name);
    }
    catch (std::out_of_range&)
    {}
    return Dimension::Type::None;
}


void PlyReader::extractProperty(Element& element)
{
    std::string word = nextWord();
    Dimension::Type type = getType(word);

    if (type != Dimension::Type::None)
    {
        std::string name = nextWord();
        if (name.empty())
            throwError("No name for property of element '" +
                element.m_name + "'.");
        element.m_properties.push_back(
            std::unique_ptr<Property>(new SimpleProperty(name, type)));
    }
    else if (word == "list")
    {
        if (element.m_name == "vertex")
            throwError("List properties are not supported for the 'vertex' "
                "element.");

        word = nextWord();
        Dimension::Type countType = getType(word);
        if (countType == Dimension::Type::None)
            throwError("No valid count type for list property of element '" +
                element.m_name + "'.");
        word = nextWord();
        Dimension::Type listType = getType(word);
        if (listType == Dimension::Type::None)
            throwError("No valid list type for list property of element '" +
                element.m_name + "'.");
        std::string name = nextWord();
        if (name.empty())
            throwError("No name for property of element '" +
                element.m_name + "'.");
        element.m_properties.push_back(
            std::unique_ptr<Property>(new ListProperty(name, countType,
                listType)));
    }
    else
        throwError("Invalid property type '" + word + "'.");
}


void PlyReader::extractProperties(Element& element)
{
    while (true)
    {
        std::string word = readLine();
        if (word == "comment" || word == "obj_info")
            continue;
        else if (word == "property")
            extractProperty(element);
        else
        {
            pushLine();
            break;
        }
    }
}


bool PlyReader::extractElement()
{
    std::string word = readLine();
    if (word == "comment" || word == "obj_info")
        return true;
    else if (word == "end_header")
    {
        pushLine();
        return false;
    }
    else if (word == "element")
    {
        std::string name = nextWord();
        if (name.empty())
            throwError("Missing element name.");
        long count = std::stol(nextWord());
        if (count < 0)
            throwError("Invalid count for element '" + name + "'.");
        m_elements.emplace_back(name, count);
        extractProperties(m_elements.back());
        return true;
    }
    throwError("Invalid keyword '" + word + "' when expecting an element.");
    return false;  // quiet compiler
}


void PlyReader::extractHeader()
{
    m_elements.clear();
    extractMagic();
    extractFormat();
    while (extractElement())
        ;
    extractEnd();
    m_dataPos = m_stream->tellg();

    for (Element& elt : m_elements)
        if (elt.m_name == "vertex")
            m_vertexElt = &elt;
    if (!m_vertexElt)
        throwError("Can't read PLY file without a 'vertex' element.");
}


std::string PlyReader::getName() const
{
    return s_info.name;
}

QuickInfo PlyReader::inspect()
{
    QuickInfo qi;

    PointTable t;
    initialize();
    addDimensions(t.layout());
    t.finalize();

    if (m_vertexElt)
    {
        // Get quick info information from the vertex element (if initialised). We ignore the bounds
        // since calculating that would require reading the entire file. We also ignore the spatial
        // reference since we don't have that.

        qi.m_valid = true;
        qi.m_pointCount = static_cast<point_count_t>(m_vertexElt->m_count);
        for (auto& prop : m_vertexElt->m_properties)
            qi.m_dimNames.push_back(prop->m_name);
    }

    // We have to read the points to actually get the bounds
    PointViewPtr v(new PointView(t));
    ready(t);
    read(v, qi.m_pointCount);
    done(t);
    for (PointRef p : *v)
    {
        float x = p.getFieldAs<float>(Dimension::Id::X);
        float y = p.getFieldAs<float>(Dimension::Id::Y);
        float z = p.getFieldAs<float>(Dimension::Id::Z);
        qi.m_bounds.grow(x, y, z);
    }

    return qi;
}

void PlyReader::initialize()
{
    m_stream = Utils::openFile(m_filename, true);
    if (!m_stream)
        throwError("Couldn't open '" + m_filename + "'.");
    try
    {
        extractHeader();
    }
    catch( ... )
    {
        Utils::closeFile(m_stream);
        m_stream = nullptr;
        throw;
    }
    Utils::closeFile(m_stream);
    m_stream = nullptr;
}


void PlyReader::addDimensions(PointLayoutPtr layout)
{
    // Override XYZ to doubles.
    layout->registerDim(Dimension::Id::X);
    layout->registerDim(Dimension::Id::Y);
    layout->registerDim(Dimension::Id::Z);

    for (auto& elt : m_elements)
    {
        if (elt.m_name == "vertex")
        {
            for (auto& prop : elt.m_properties)
            {
                auto vprop = static_cast<SimpleProperty *>(prop.get());
                layout->registerOrAssignDim(vprop->m_name, vprop->m_type);
                vprop->setDim(
                    layout->registerOrAssignDim(vprop->m_name, vprop->m_type));
            }
            return;
        }
    }
    throwError("No 'vertex' element in header.");
}


bool PlyReader::readProperty(Property *prop, PointRef& point)
{
    if (!m_stream->good())
        return false;
    prop->read(m_stream, m_format, point);
    return true;
}


void PlyReader::SimpleProperty::read(std::istream *stream,
    PlyReader::Format format, PointRef& point)
{
    if (format == Format::Ascii)
    {
        double d;
        *stream >> d;
        point.setField(m_dim, d);
    }
    else if (format == Format::BinaryLe)
    {
        ILeStream in(stream);
        Everything e = Utils::extractDim(in, m_type);
        point.setField(m_dim, m_type, &e);
    }
    else if (format == Format::BinaryBe)
    {
        IBeStream in(stream);
        Everything e = Utils::extractDim(in, m_type);
        point.setField(m_dim, m_type, &e);
    }
}


// Right now we don't support list properties for point data.  We just
// read the data and throw it away.
void PlyReader::ListProperty::read(std::istream *stream,
    PlyReader::Format format, PointRef& point)
{
    if (format == Format::Ascii)
    {
        size_t cnt;
        *stream >> cnt;

        double d;
        while (cnt--)
            *stream >> d;
    }
    else if (format == Format::BinaryLe)
    {
        ILeStream istream(stream);
        Everything e = Utils::extractDim(istream, m_countType);
        size_t cnt = (size_t)Utils::toDouble(e, m_countType);
        cnt *= Dimension::size(m_listType);
        istream.seek(cnt, std::ios_base::cur);
    }
    else if (format == Format::BinaryBe)
    {
        IBeStream istream(stream);
        Everything e = Utils::extractDim(istream, m_countType);
        size_t cnt = (size_t)Utils::toDouble(e, m_countType);
        cnt *= Dimension::size(m_listType);
        istream.seek(cnt, std::ios_base::cur);
    }
}


void PlyReader::readElement(Element& elt, PointRef& point)
{
    for (auto& prop : elt.m_properties)
        if (!readProperty(prop.get(), point))
            throwError("Error reading data for point/element " +
                std::to_string(point.pointId()) + ".");
}


void PlyReader::ready(PointTableRef table)
{
    m_stream = Utils::openFile(m_filename, true);
    if (m_stream)
        m_stream->seekg(m_dataPos);
    for (Element& elt : m_elements)
    {
        if (&elt == m_vertexElt)
            break;

        // We read an element into point 0.  Since the element's properties
        // weren't registered as dimensions, we'll try to write the data
        // to a NULL dimension, which is a noop.
        // This essentially just gets us to the vertex element.
        // In binary mode, this is all silliness, since we should be able
        // to seek right where we want to go, but in text mode, you've got
        // to go through the data.
        PointRef point(table, 0);
        for (PointId idx = 0; idx < elt.m_count; ++idx)
            readElement(elt, point);
    }
    m_index = 0;
}


bool PlyReader::processOne(PointRef& point)
{
    if (m_index < m_vertexElt->m_count)
    {
        readElement(*m_vertexElt, point);
        m_index++;
        return true;
    }
    return false;
}


// We're just reading the vertex element here.
point_count_t PlyReader::read(PointViewPtr view, point_count_t num)
{
    point_count_t cnt(0);

    PointRef point(view->point(0));
    for (PointId idx = 0; idx < m_vertexElt->m_count && idx < num; ++idx)
    {
        point.setPointId(idx);
        processOne(point);
        cnt++;
    }
    return cnt;
}


void PlyReader::done(PointTableRef table)
{
    Utils::closeFile(m_stream);
}

} // namespace pdal
