// Copyright 2016 The Draco Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "draco/io/obj_decoder.h"

#include <cctype>
#include <cmath>
#include <fstream>

#include "draco/io/file_utils.h"
#include "draco/io/parser_utils.h"
#include "draco/metadata/geometry_metadata.h"

namespace draco {

ObjDecoder::ObjDecoder()
    : counting_mode_(true),
      num_obj_faces_(0),
      num_positions_(0),
      num_tex_coords_(0),
      num_normals_(0),
      num_materials_(0),
      last_sub_obj_id_(0),
      pos_att_id_(-1),
      tex_att_id_(-1),
      norm_att_id_(-1),
      material_att_id_(-1),
      sub_obj_att_id_(-1),
      deduplicate_input_values_(true),
      last_material_id_(0),
      use_metadata_(false),
      out_mesh_(nullptr),
      out_point_cloud_(nullptr) {}

Status ObjDecoder::DecodeFromFile(const std::string &file_name,
                                  Mesh *out_mesh) {
  out_mesh_ = out_mesh;
  return DecodeFromFile(file_name, static_cast<PointCloud *>(out_mesh));
}

Status ObjDecoder::DecodeFromFile(const std::string &file_name,
                                  PointCloud *out_point_cloud) {
  std::ifstream file(file_name, std::ios::binary);
  if (!file)
    return Status(Status::IO_ERROR);
  // Read the whole file into a buffer.
  auto pos0 = file.tellg();
  file.seekg(0, std::ios::end);
  auto file_size = file.tellg() - pos0;
  if (file_size == 0)
    return Status(Status::IO_ERROR);
  file.seekg(0, std::ios::beg);
  std::vector<char> data(file_size);
  file.read(&data[0], file_size);
  buffer_.Init(&data[0], file_size);

  out_point_cloud_ = out_point_cloud;
  input_file_name_ = file_name;
  return DecodeInternal();
}

Status ObjDecoder::DecodeFromBuffer(DecoderBuffer *buffer, Mesh *out_mesh) {
  out_mesh_ = out_mesh;
  return DecodeFromBuffer(buffer, static_cast<PointCloud *>(out_mesh));
}

Status ObjDecoder::DecodeFromBuffer(DecoderBuffer *buffer,
                                    PointCloud *out_point_cloud) {
  out_point_cloud_ = out_point_cloud;
  buffer_.Init(buffer->data_head(), buffer->remaining_size());
  return DecodeInternal();
}

Status ObjDecoder::DecodeInternal() {
  // In the first pass, count the number of different elements in the geometry.
  // In case the desired output is just a point cloud (i.e., when
  // out_mesh_ == nullptr) the decoder will ignore all information about the
  // connectivity that may be included in the source data.
  counting_mode_ = true;
  ResetCounters();
  material_name_to_id_.clear();
  last_sub_obj_id_ = 0;
  // Parse all lines.
  Status status(Status::OK);
  while (ParseDefinition(&status) && status.ok()) {
  }
  if (!status.ok())
    return status;
  bool use_identity_mapping = false;
  if (num_obj_faces_ == 0) {
    // Mesh has no faces. In this case we try to read the geometry as a point
    // cloud where every attribute entry is a point.

    // Ensure the number of all entries is same for all attributes.
    if (num_positions_ == 0)
      return Status(Status::DRACO_ERROR, "No position attribute");
    if (num_tex_coords_ > 0 && num_tex_coords_ != num_positions_)
      return Status(Status::DRACO_ERROR,
                    "Invalid number of texture coordinates for a point cloud");
    if (num_normals_ > 0 && num_normals_ != num_positions_)
      return Status(Status::DRACO_ERROR,
                    "Invalid number of normals for a point cloud");

    out_mesh_ = nullptr;  // Treat the output geometry as a point cloud.
    use_identity_mapping = true;
  }

  // Initialize point cloud and mesh properties.
  if (out_mesh_) {
    // Start decoding a mesh with the given number of faces. For point clouds we
    // silently ignore all data about the mesh connectivity.
    out_mesh_->SetNumFaces(num_obj_faces_);
  }
  if (num_obj_faces_ > 0) {
    out_point_cloud_->set_num_points(3 * num_obj_faces_);
  } else {
    out_point_cloud_->set_num_points(num_positions_);
  }

  // Add attributes if they are present in the input data.
  if (num_positions_ > 0) {
    GeometryAttribute va;
    va.Init(GeometryAttribute::POSITION, nullptr, 3, DT_FLOAT32, false,
            sizeof(float) * 3, 0);
    pos_att_id_ = out_point_cloud_->AddAttribute(va, use_identity_mapping,
                                                 num_positions_);
  }
  if (num_tex_coords_ > 0) {
    GeometryAttribute va;
    va.Init(GeometryAttribute::TEX_COORD, nullptr, 2, DT_FLOAT32, false,
            sizeof(float) * 2, 0);
    tex_att_id_ = out_point_cloud_->AddAttribute(va, use_identity_mapping,
                                                 num_tex_coords_);
  }
  if (num_normals_ > 0) {
    GeometryAttribute va;
    va.Init(GeometryAttribute::NORMAL, nullptr, 3, DT_FLOAT32, false,
            sizeof(float) * 3, 0);
    norm_att_id_ =
        out_point_cloud_->AddAttribute(va, use_identity_mapping, num_normals_);
  }
  if (num_materials_ > 0 && num_obj_faces_ > 0) {
    GeometryAttribute va;
    const auto geometry_attribute_type = GeometryAttribute::GENERIC;
    if (num_materials_ < 256) {
      va.Init(geometry_attribute_type, nullptr, 1, DT_UINT8, false, 1, 0);
    } else if (num_materials_ < (1 << 16)) {
      va.Init(geometry_attribute_type, nullptr, 1, DT_UINT16, false, 2, 0);
    } else {
      va.Init(geometry_attribute_type, nullptr, 1, DT_UINT32, false, 4, 0);
    }
    material_att_id_ =
        out_point_cloud_->AddAttribute(va, false, num_materials_);

    // Fill the material entries.
    for (int i = 0; i < num_materials_; ++i) {
      const AttributeValueIndex avi(i);
      out_point_cloud_->attribute(material_att_id_)->SetAttributeValue(avi, &i);
    }

    if (use_metadata_) {
      // Use metadata to store the name of materials.
      std::unique_ptr<AttributeMetadata> material_metadata =
          std::unique_ptr<AttributeMetadata>(new AttributeMetadata());
      material_metadata->AddEntryString("name", "material");
      // Add all material names.
      for (const auto &itr : material_name_to_id_) {
        material_metadata->AddEntryInt(itr.first, itr.second);
      }
      if (!material_file_name_.empty()) {
        material_metadata->AddEntryString("file_name", material_file_name_);
      }

      out_point_cloud_->AddAttributeMetadata(material_att_id_,
                                             std::move(material_metadata));
    }
  }
  if (!obj_name_to_id_.empty() && num_obj_faces_ > 0) {
    GeometryAttribute va;
    if (obj_name_to_id_.size() < 256) {
      va.Init(GeometryAttribute::GENERIC, nullptr, 1, DT_UINT8, false, 1, 0);
    } else if (obj_name_to_id_.size() < (1 << 16)) {
      va.Init(GeometryAttribute::GENERIC, nullptr, 1, DT_UINT16, false, 2, 0);
    } else {
      va.Init(GeometryAttribute::GENERIC, nullptr, 1, DT_UINT32, false, 4, 0);
    }
    sub_obj_att_id_ = out_point_cloud_->AddAttribute(
        va, false, static_cast<uint32_t>(obj_name_to_id_.size()));
    // Fill the sub object id entries.
    for (const auto &itr : obj_name_to_id_) {
      const AttributeValueIndex i(itr.second);
      out_point_cloud_->attribute(sub_obj_att_id_)->SetAttributeValue(i, &i);
    }
    if (use_metadata_) {
      // Use metadata to store the name of materials.
      std::unique_ptr<AttributeMetadata> sub_obj_metadata =
          std::unique_ptr<AttributeMetadata>(new AttributeMetadata());
      sub_obj_metadata->AddEntryString("name", "sub_obj");
      // Add all sub object names.
      for (const auto &itr : obj_name_to_id_) {
        const AttributeValueIndex i(itr.second);
        sub_obj_metadata->AddEntryInt(itr.first, itr.second);
      }
      out_point_cloud_->AddAttributeMetadata(sub_obj_att_id_,
                                             std::move(sub_obj_metadata));
    }
  }

  // Perform a second iteration of parsing and fill all the data.
  counting_mode_ = false;
  ResetCounters();
  // Start parsing from the beginning of the buffer again.
  buffer()->StartDecodingFrom(0);
  while (ParseDefinition(&status) && status.ok()) {
  }
  if (!status.ok())
    return status;
  if (out_mesh_) {
    // Add faces with identity mapping between vertex and corner indices.
    // Duplicate vertices will get removed later.
    Mesh::Face face;
    for (FaceIndex i(0); i < num_obj_faces_; ++i) {
      for (int c = 0; c < 3; ++c)
        face[c] = 3 * i.value() + c;
      out_mesh_->SetFace(i, face);
    }
  }

#ifdef DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED
  if (deduplicate_input_values_) {
    out_point_cloud_->DeduplicateAttributeValues();
  }
#endif
#ifdef DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED
  out_point_cloud_->DeduplicatePointIds();
#endif
  return status;
}

void ObjDecoder::ResetCounters() {
  num_obj_faces_ = 0;
  num_positions_ = 0;
  num_tex_coords_ = 0;
  num_normals_ = 0;
  last_material_id_ = 0;
  last_sub_obj_id_ = 0;
}

bool ObjDecoder::ParseDefinition(Status *status) {
  char c;
  parser::SkipWhitespace(buffer());
  if (!buffer()->Peek(&c)) {
    // End of file reached?.
    return false;
  }
  if (c == '#') {
    // Comment, ignore the line.
    parser::SkipLine(buffer());
    return true;
  }
  if (ParseVertexPosition(status))
    return true;
  if (ParseNormal(status))
    return true;
  if (ParseTexCoord(status))
    return true;
  if (ParseFace(status))
    return true;
  if (ParseMaterial(status))
    return true;
  if (ParseMaterialLib(status))
    return true;
  if (ParseObject(status))
    return true;
  // No known definition was found. Ignore the line.
  parser::SkipLine(buffer());
  return true;
}

bool ObjDecoder::ParseVertexPosition(Status *status) {
  std::array<char, 2> c;
  if (!buffer()->Peek(&c)) {
    return false;
  }
  if (c[0] != 'v' || c[1] != ' ')
    return false;
  // Vertex definition found!
  buffer()->Advance(2);
  if (!counting_mode_) {
    // Parse three float numbers for vertex position coordinates.
    float val[3];
    for (int i = 0; i < 3; ++i) {
      parser::SkipWhitespace(buffer());
      if (!parser::ParseFloat(buffer(), val + i)) {
        *status = Status(Status::DRACO_ERROR, "Failed to parse a float number");
        // The definition is processed so return true.
        return true;
      }
    }
    out_point_cloud_->attribute(pos_att_id_)
        ->SetAttributeValue(AttributeValueIndex(num_positions_), val);
  }
  ++num_positions_;
  parser::SkipLine(buffer());
  return true;
}

bool ObjDecoder::ParseNormal(Status *status) {
  std::array<char, 2> c;
  if (!buffer()->Peek(&c)) {
    return false;
  }
  if (c[0] != 'v' || c[1] != 'n')
    return false;
  // Normal definition found!
  buffer()->Advance(2);
  if (!counting_mode_) {
    // Parse three float numbers for the normal vector.
    float val[3];
    for (int i = 0; i < 3; ++i) {
      parser::SkipWhitespace(buffer());
      if (!parser::ParseFloat(buffer(), val + i)) {
        *status = Status(Status::DRACO_ERROR, "Failed to parse a float number");
        // The definition is processed so return true.
        return true;
      }
    }
    out_point_cloud_->attribute(norm_att_id_)
        ->SetAttributeValue(AttributeValueIndex(num_normals_), val);
  }
  ++num_normals_;
  parser::SkipLine(buffer());
  return true;
}

bool ObjDecoder::ParseTexCoord(Status *status) {
  std::array<char, 2> c;
  if (!buffer()->Peek(&c)) {
    return false;
  }
  if (c[0] != 'v' || c[1] != 't')
    return false;
  // Texture coord definition found!
  buffer()->Advance(2);
  if (!counting_mode_) {
    // Parse two float numbers for the texture coordinate.
    float val[2];
    for (int i = 0; i < 2; ++i) {
      parser::SkipWhitespace(buffer());
      if (!parser::ParseFloat(buffer(), val + i)) {
        *status = Status(Status::DRACO_ERROR, "Failed to parse a float number");
        // The definition is processed so return true.
        return true;
      }
    }
    out_point_cloud_->attribute(tex_att_id_)
        ->SetAttributeValue(AttributeValueIndex(num_tex_coords_), val);
  }
  ++num_tex_coords_;
  parser::SkipLine(buffer());
  return true;
}

bool ObjDecoder::ParseFace(Status *status) {
  char c;
  if (!buffer()->Peek(&c)) {
    return false;
  }
  if (c != 'f')
    return false;
  // Face definition found!
  buffer()->Advance(1);
  if (!counting_mode_) {
    std::array<int32_t, 3> indices[4];
    // Parse face indices (we try to look for up to four to support quads).
    int num_valid_indices = 0;
    for (int i = 0; i < 4; ++i) {
      if (!ParseVertexIndices(&indices[i])) {
        if (i == 3) {
          break;  // It's OK if there is no fourth vertex index.
        }
        *status = Status(Status::DRACO_ERROR, "Failed to parse vertex indices");
        return true;
      }
      ++num_valid_indices;
    }
    // Process the first face.
    for (int i = 0; i < 3; ++i) {
      const PointIndex vert_id(3 * num_obj_faces_ + i);
      MapPointToVertexIndices(vert_id, indices[i]);
    }
    ++num_obj_faces_;
    if (num_valid_indices == 4) {
      // Add an additional triangle for the quad.
      //
      //   3----2
      //   |  / |
      //   | /  |
      //   0----1
      //
      const PointIndex vert_id(3 * num_obj_faces_);
      MapPointToVertexIndices(vert_id, indices[0]);
      MapPointToVertexIndices(vert_id + 1, indices[2]);
      MapPointToVertexIndices(vert_id + 2, indices[3]);
      ++num_obj_faces_;
    }
  } else {
    // We are in the counting mode.
    // We need to determine how many triangles are in the obj face.
    // Go over the line and check how many gaps there are between non-empty
    // sub-strings.
    parser::SkipWhitespace(buffer());
    int num_indices = 0;
    bool is_end = false;
    while (buffer()->Peek(&c) && c != '\n') {
      if (parser::PeekWhitespace(buffer(), &is_end)) {
        buffer()->Advance(1);
      } else {
        // Non-whitespace reached.. assume it's index declaration, skip it.
        num_indices++;
        while (!parser::PeekWhitespace(buffer(), &is_end) && !is_end) {
          buffer()->Advance(1);
        }
      }
    }
    if (num_indices < 3 || num_indices > 4) {
      *status =
          Status(Status::DRACO_ERROR, "Invalid number of indices on a face");
      return false;
    }
    // Either one or two new triangles.
    num_obj_faces_ += num_indices - 2;
  }
  parser::SkipLine(buffer());
  return true;
}

bool ObjDecoder::ParseMaterialLib(Status *status) {
  // Allow only one material library per file for now.
  if (material_name_to_id_.size() > 0)
    return false;
  std::array<char, 6> c;
  if (!buffer()->Peek(&c)) {
    return false;
  }
  if (std::memcmp(&c[0], "mtllib", 6) != 0)
    return false;
  buffer()->Advance(6);
  DecoderBuffer line_buffer = parser::ParseLineIntoDecoderBuffer(buffer());
  parser::SkipWhitespace(&line_buffer);
  material_file_name_.clear();
  if (!parser::ParseString(&line_buffer, &material_file_name_)) {
    *status = Status(Status::DRACO_ERROR, "Failed to parse material file name");
    return true;
  }
  parser::SkipLine(&line_buffer);

  if (material_file_name_.size() > 0) {
    if (!ParseMaterialFile(material_file_name_, status)) {
      // Silently ignore problems with material files for now.
      return true;
    }
  }
  return true;
}

bool ObjDecoder::ParseMaterial(Status * /* status */) {
  // In second pass, skip when we don't use materials.
  if (!counting_mode_ && material_att_id_ < 0)
    return false;
  std::array<char, 6> c;
  if (!buffer()->Peek(&c)) {
    return false;
  }
  if (std::memcmp(&c[0], "usemtl", 6) != 0)
    return false;
  buffer()->Advance(6);
  DecoderBuffer line_buffer = parser::ParseLineIntoDecoderBuffer(buffer());
  parser::SkipWhitespace(&line_buffer);
  std::string mat_name;
  parser::ParseLine(&line_buffer, &mat_name);
  if (mat_name.length() == 0)
    return false;
  auto it = material_name_to_id_.find(mat_name);
  if (it == material_name_to_id_.end()) {
    // In first pass, materials found in obj that's not in the .mtl file
    // will be added to the list.
    last_material_id_ = num_materials_;
    material_name_to_id_[mat_name] = num_materials_++;

    return true;
  }
  last_material_id_ = it->second;
  return true;
}

bool ObjDecoder::ParseObject(Status *status) {
  std::array<char, 2> c;
  if (!buffer()->Peek(&c)) {
    return false;
  }
  if (std::memcmp(&c[0], "o ", 2) != 0)
    return false;
  buffer()->Advance(1);
  DecoderBuffer line_buffer = parser::ParseLineIntoDecoderBuffer(buffer());
  parser::SkipWhitespace(&line_buffer);
  std::string obj_name;
  if (!parser::ParseString(&line_buffer, &obj_name))
    return false;
  if (obj_name.length() == 0)
    return true;  // Ignore empty name entries.
  auto it = obj_name_to_id_.find(obj_name);
  if (it == obj_name_to_id_.end()) {
    const int num_obj = static_cast<int>(obj_name_to_id_.size());
    obj_name_to_id_[obj_name] = num_obj;
    last_sub_obj_id_ = num_obj;
  } else {
    last_sub_obj_id_ = it->second;
  }
  return true;
}

bool ObjDecoder::ParseVertexIndices(std::array<int32_t, 3> *out_indices) {
  // Parsed attribute indices can be in format:
  // 1. POS_INDEX
  // 2. POS_INDEX/TEX_COORD_INDEX
  // 3. POS_INDEX/TEX_COORD_INDEX/NORMAL_INDEX
  // 4. POS_INDEX//NORMAL_INDEX
  parser::SkipCharacters(buffer(), " \t");
  if (!parser::ParseSignedInt(buffer(), &(*out_indices)[0]) ||
      (*out_indices)[0] == 0)
    return false;  // Position index must be present and valid.
  (*out_indices)[1] = (*out_indices)[2] = 0;
  char ch;
  if (!buffer()->Peek(&ch))
    return true;  // It may be OK if we cannot read any more characters.
  if (ch != '/')
    return true;
  buffer()->Advance(1);
  // Check if we should skip texture index or not.
  if (!buffer()->Peek(&ch))
    return false;  // Here, we should be always able to read the next char.
  if (ch != '/') {
    // Must be texture coord index.
    if (!parser::ParseSignedInt(buffer(), &(*out_indices)[1]) ||
        (*out_indices)[1] == 0)
      return false;  // Texture index must be present and valid.
  }
  if (!buffer()->Peek(&ch))
    return true;
  if (ch == '/') {
    buffer()->Advance(1);
    // Read normal index.
    if (!parser::ParseSignedInt(buffer(), &(*out_indices)[2]) ||
        (*out_indices)[2] == 0)
      return false;  // Normal index must be present and valid.
  }
  return true;
}

void ObjDecoder::MapPointToVertexIndices(
    PointIndex vert_id, const std::array<int32_t, 3> &indices) {
  // Use face entries to store mapping between vertex and attribute indices
  // (positions, texture coordinates and normal indices).
  // Any given index is used when indices[x] != 0. For positive values, the
  // point is mapped directly to the specified attribute index. Negative input
  // indices indicate addressing from the last element (e.g. -1 is the last
  // attribute value of a given type, -2 the second last, etc.).
  if (indices[0] > 0) {
    out_point_cloud_->attribute(pos_att_id_)
        ->SetPointMapEntry(vert_id, AttributeValueIndex(indices[0] - 1));
  } else if (indices[0] < 0) {
    out_point_cloud_->attribute(pos_att_id_)
        ->SetPointMapEntry(vert_id,
                           AttributeValueIndex(num_positions_ + indices[0]));
  }

  if (tex_att_id_ >= 0) {
    if (indices[1] > 0) {
      out_point_cloud_->attribute(tex_att_id_)
          ->SetPointMapEntry(vert_id, AttributeValueIndex(indices[1] - 1));
    } else if (indices[1] < 0) {
      out_point_cloud_->attribute(tex_att_id_)
          ->SetPointMapEntry(vert_id,
                             AttributeValueIndex(num_tex_coords_ + indices[1]));
    } else {
      // Texture index not provided but expected. Insert 0 entry as the
      // default value.
      out_point_cloud_->attribute(tex_att_id_)
          ->SetPointMapEntry(vert_id, AttributeValueIndex(0));
    }
  }

  if (norm_att_id_ >= 0) {
    if (indices[2] > 0) {
      out_point_cloud_->attribute(norm_att_id_)
          ->SetPointMapEntry(vert_id, AttributeValueIndex(indices[2] - 1));
    } else if (indices[2] < 0) {
      out_point_cloud_->attribute(norm_att_id_)
          ->SetPointMapEntry(vert_id,
                             AttributeValueIndex(num_normals_ + indices[2]));
    } else {
      // Normal index not provided but expected. Insert 0 entry as the default
      // value.
      out_point_cloud_->attribute(norm_att_id_)
          ->SetPointMapEntry(vert_id, AttributeValueIndex(0));
    }
  }

  // Assign material index to the point if it is available.
  if (material_att_id_ >= 0) {
    out_point_cloud_->attribute(material_att_id_)
        ->SetPointMapEntry(vert_id, AttributeValueIndex(last_material_id_));
  }

  // Assign sub-object index to the point if it is available.
  if (sub_obj_att_id_ >= 0) {
    out_point_cloud_->attribute(sub_obj_att_id_)
        ->SetPointMapEntry(vert_id, AttributeValueIndex(last_sub_obj_id_));
  }
}

bool ObjDecoder::ParseMaterialFile(const std::string &file_name,
                                   Status *status) {
  const std::string full_path = GetFullPath(file_name, input_file_name_);
  std::ifstream file(full_path, std::ios::binary);
  if (!file)
    return false;
  // Read the whole file into a buffer.
  file.seekg(0, std::ios::end);
  const std::string::size_type file_size = file.tellg();
  if (file_size == 0)
    return false;
  file.seekg(0, std::ios::beg);
  std::vector<char> data(file_size);
  file.read(&data[0], file_size);

  // Backup the original decoder buffer.
  DecoderBuffer old_buffer = buffer_;

  buffer_.Init(&data[0], file_size);

  num_materials_ = 0;
  while (ParseMaterialFileDefinition(status)) {
  }

  // Restore the original buffer.
  buffer_ = old_buffer;
  return true;
}

bool ObjDecoder::ParseMaterialFileDefinition(Status * /* status */) {
  char c;
  parser::SkipWhitespace(buffer());
  if (!buffer()->Peek(&c)) {
    // End of file reached?.
    return false;
  }
  if (c == '#') {
    // Comment, ignore the line.
    parser::SkipLine(buffer());
    return true;
  }
  std::string str;
  if (!parser::ParseString(buffer(), &str))
    return false;
  if (str == "newmtl") {
    parser::SkipWhitespace(buffer());
    parser::ParseLine(buffer(), &str);
    if (str.empty())
      return false;
    // Add new material to our map.
    material_name_to_id_[str] = num_materials_++;
  }
  return true;
}

}  // namespace draco
