#include <Eigen/Dense>
#include <Eigen/Geometry>
#include <array>
#include <cmath>
#include <cstdlib>
#include <lodepng.h>
#include <memory>
#include <robot_design/render.h>
#include <stdexcept>
#include <string>
#include <vector>

namespace robot_design {

const VertexAttribute ATTRIB_POSITION(0, "model_position");
const VertexAttribute ATTRIB_NORMAL(1, "model_normal");
const VertexAttribute ATTRIB_TEX_COORD(2, "model_tex_coord");

Program::Program(const std::string &vertex_shader_source,
                 const std::string &fragment_shader_source)
    : program_(0), vertex_shader_(0), fragment_shader_(0) {
  // Create vertex shader
  vertex_shader_ = glCreateShader(GL_VERTEX_SHADER);
  const GLchar *vertex_shader_source_ptr = vertex_shader_source.c_str();
  glShaderSource(vertex_shader_, 1, &vertex_shader_source_ptr, NULL);
  glCompileShader(vertex_shader_);
  // Check for compile errors
  GLint status;
  glGetShaderiv(vertex_shader_, GL_COMPILE_STATUS, &status);
  if (!status) {
    char buffer[512];
    glGetShaderInfoLog(vertex_shader_, sizeof(buffer), NULL, buffer);
    throw std::runtime_error(std::string("Failed to compile vertex shader: ") +
                             buffer);
  }

  // Create fragment shader
  // Fragment shader is optional (source may be an empty string)
  if (!fragment_shader_source.empty()) {
    fragment_shader_ = glCreateShader(GL_FRAGMENT_SHADER);
    const GLchar *fragment_shader_source_ptr = fragment_shader_source.c_str();
    glShaderSource(fragment_shader_, 1, &fragment_shader_source_ptr, NULL);
    glCompileShader(fragment_shader_);
    // Check for compile errors
    glGetShaderiv(fragment_shader_, GL_COMPILE_STATUS, &status);
    if (!status) {
      char buffer[512];
      glGetShaderInfoLog(fragment_shader_, sizeof(buffer), NULL, buffer);
      throw std::runtime_error(
          std::string("Failed to compile fragment shader: ") + buffer);
    }
  }

  // Create program
  program_ = glCreateProgram();
  glAttachShader(program_, vertex_shader_);
  if (fragment_shader_) {
    glAttachShader(program_, fragment_shader_);
  }

  // Define fixed attribute indices
  glBindAttribLocation(program_, ATTRIB_POSITION.index_,
                       ATTRIB_POSITION.name_.c_str());
  glBindAttribLocation(program_, ATTRIB_NORMAL.index_,
                       ATTRIB_NORMAL.name_.c_str());
  glBindAttribLocation(program_, ATTRIB_TEX_COORD.index_,
                       ATTRIB_TEX_COORD.name_.c_str());

  glLinkProgram(program_);
  // Check for link errors
  glGetProgramiv(program_, GL_LINK_STATUS, &status);
  if (!status) {
    char buffer[512];
    glGetProgramInfoLog(program_, sizeof(buffer), NULL, buffer);
    throw std::runtime_error(std::string("Failed to link shader program: ") +
                             buffer);
  }

  // Find uniform indices
  proj_matrix_index_ = glGetUniformLocation(program_, "proj_matrix");
  view_matrix_index_ = glGetUniformLocation(program_, "view_matrix");
  tex_coords_matrix_index_ =
      glGetUniformLocation(program_, "tex_coords_matrix");
  model_view_matrix_index_ =
      glGetUniformLocation(program_, "model_view_matrix");
  normal_matrix_index_ = glGetUniformLocation(program_, "normal_matrix");
  proc_texture_type_index_ =
      glGetUniformLocation(program_, "proc_texture_type");
  object_color_index_ = glGetUniformLocation(program_, "object_color");
  world_light_dir_index_ = glGetUniformLocation(program_, "world_light_dir");
  light_proj_matrix_index_ =
      glGetUniformLocation(program_, "light_proj_matrix");
  light_model_view_matrices_index_ =
      glGetUniformLocation(program_, "light_model_view_matrices");
  light_color_index_ = glGetUniformLocation(program_, "light_color");
  shadow_map_index_ = glGetUniformLocation(program_, "shadow_map");
  msdf_index_ = glGetUniformLocation(program_, "msdf");
  cascade_far_splits_index_ =
      glGetUniformLocation(program_, "cascade_far_splits");
}

Program::~Program() {
  glDetachShader(program_, fragment_shader_);
  glDetachShader(program_, vertex_shader_);
  glDeleteProgram(program_);
  glDeleteShader(fragment_shader_);
  glDeleteShader(vertex_shader_);
}

Mesh::Mesh(GLenum usage)
    : usage_(usage), vertex_array_(0), position_buffer_(0), normal_buffer_(0),
      tex_coord_buffer_(0), index_buffer_(0) {
  // Create vertex array object (VAO)
  glGenVertexArrays(1, &vertex_array_);
}

Mesh::~Mesh() {
  glDeleteBuffers(1, &index_buffer_);
  glDeleteBuffers(1, &tex_coord_buffer_);
  glDeleteBuffers(1, &normal_buffer_);
  glDeleteBuffers(1, &position_buffer_);
  glDeleteVertexArrays(1, &vertex_array_);
}

void Mesh::setPositions(const std::vector<GLfloat> &positions) {
  bind();
  if (!position_buffer_) {
    glGenBuffers(1, &position_buffer_);
  }
  glBindBuffer(GL_ARRAY_BUFFER, position_buffer_);
  glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(positions[0]),
               positions.data(), usage_);
  glVertexAttribPointer(ATTRIB_POSITION.index_, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(ATTRIB_POSITION.index_);
}

void Mesh::setNormals(const std::vector<GLfloat> &normals) {
  bind();
  if (!normal_buffer_) {
    glGenBuffers(1, &normal_buffer_);
  }
  glBindBuffer(GL_ARRAY_BUFFER, normal_buffer_);
  glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(normals[0]),
               normals.data(), usage_);
  glVertexAttribPointer(ATTRIB_NORMAL.index_, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(ATTRIB_NORMAL.index_);
}

void Mesh::setTexCoords(const std::vector<GLfloat> &tex_coords) {
  bind();
  if (!tex_coord_buffer_) {
    glGenBuffers(1, &tex_coord_buffer_);
  }
  glBindBuffer(GL_ARRAY_BUFFER, tex_coord_buffer_);
  glBufferData(GL_ARRAY_BUFFER, tex_coords.size() * sizeof(tex_coords[0]),
               tex_coords.data(), usage_);
  glVertexAttribPointer(ATTRIB_TEX_COORD.index_, 2, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(ATTRIB_TEX_COORD.index_);
}

void Mesh::setIndices(const std::vector<GLint> &indices) {
  bind();
  if (!index_buffer_) {
    glGenBuffers(1, &index_buffer_);
  }
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(indices[0]),
               indices.data(), usage_);
  index_count_ = indices.size();
}

Texture2D::Texture2D(GLenum target, GLint level, GLint internal_format,
                     GLsizei width, GLsizei height, GLenum format, GLenum type,
                     const GLvoid *data)
    : target_(target), texture_(0) {
  glGenTextures(1, &texture_);
  glBindTexture(target, texture_);
  glTexImage2D(target, level, internal_format, width, height, 0, format, type,
               data);
}

Texture2D::~Texture2D() { glDeleteTextures(1, &texture_); }

void Texture2D::setParameter(GLenum name, GLint value) const {
  bind();
  glTexParameteri(target_, name, value);
}

void Texture2D::getImage(unsigned char *pixels) const {
  bind();
  glGetTexImage(target_, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
}

Texture3D::Texture3D(GLenum target, GLint level, GLint internal_format,
                     GLsizei width, GLsizei height, GLsizei depth,
                     GLenum format, GLenum type, const GLvoid *data)
    : target_(target), texture_(0) {
  glGenTextures(1, &texture_);
  glBindTexture(target, texture_);
  glTexImage3D(target, level, internal_format, width, height, depth, 0, format,
               type, data);
}

Texture3D::~Texture3D() { glDeleteTextures(1, &texture_); }

void Texture3D::setParameter(GLenum name, GLint value) const {
  bind();
  glTexParameteri(target_, name, value);
}

void Texture3D::getImage(unsigned char *pixels) const {
  bind();
  glGetTexImage(target_, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
}

Framebuffer::Framebuffer() : framebuffer_(0) {
  glGenFramebuffers(1, &framebuffer_);
  glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
  // Necessary to make framebuffer complete without a color attachment
  glDrawBuffer(GL_NONE);
  glReadBuffer(GL_NONE);
}

Framebuffer::~Framebuffer() { glDeleteFramebuffers(1, &framebuffer_); }

void Framebuffer::attachColorTexture(const Texture2D &color_texture) const {
  bind();
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                         color_texture.target_, color_texture.texture_, 0);
  glDrawBuffer(GL_COLOR_ATTACHMENT0);
  glReadBuffer(GL_COLOR_ATTACHMENT0);
}

void Framebuffer::attachColorTextureLayer(const Texture3D &color_texture,
                                          GLint layer) const {
  bind();
  glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                            color_texture.texture_, 0, layer);
  glDrawBuffer(GL_COLOR_ATTACHMENT0);
  glReadBuffer(GL_COLOR_ATTACHMENT0);
}

void Framebuffer::attachDepthTexture(const Texture2D &depth_texture) const {
  bind();
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                         depth_texture.target_, depth_texture.texture_, 0);
}

void Framebuffer::attachDepthTextureLayer(const Texture3D &depth_texture,
                                          GLint layer) const {
  bind();
  glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                            depth_texture.texture_, 0, layer);
}

Eigen::Matrix4f CameraParameters::getProjMatrix() const {
  Eigen::Matrix4f proj_matrix;
  makePerspectiveProjection(aspect_ratio_, z_near_, z_far_, fov_, proj_matrix);
  return proj_matrix;
}

Eigen::Matrix4f CameraParameters::getViewMatrix() const {
  Eigen::Affine3f view_transform(
      Eigen::Translation3f(0.0f, 0.0f, -distance_) *
      Eigen::AngleAxisf(-pitch_, Eigen::Vector3f::UnitX()) *
      Eigen::AngleAxisf(-yaw_, Eigen::Vector3f::UnitY()) *
      Eigen::Translation3f(-position_));
  return view_transform.matrix();
}

DirectionalLight::DirectionalLight(const Eigen::Vector3f &color,
                                   const Eigen::Vector3f &dir,
                                   const Eigen::Vector3f &up, GLsizei sm_width,
                                   GLsizei sm_height, int sm_cascade_count)
    : color_(color), dir_(dir.normalized()), sm_width_(sm_width),
      sm_height_(sm_height), sm_cascade_count_(sm_cascade_count) {
  makeOrthographicProjection(/*aspect_ratio=*/1.0f, /*z_near=*/-100.0f,
                             /*z_far=*/100.0f, /*matrix=*/proj_matrix_);
  view_matrices_.resize(4, 4 * sm_cascade_count);
  sm_cascade_splits_.resize(sm_cascade_count + 1);
  Eigen::Vector3f norm_dir = dir_;
  Eigen::Vector3f norm_up = (up - norm_dir * up.dot(norm_dir)).normalized();
  Eigen::Matrix3f inv_view_rot_matrix;
  // clang-format off
  inv_view_rot_matrix << norm_up.cross(norm_dir),
                         norm_up,
                         norm_dir;
  // clang-format on
  view_rot_matrix_ = inv_view_rot_matrix.transpose();

  sm_depth_array_texture_ = std::make_shared<Texture3D>(
      GL_TEXTURE_2D_ARRAY, 0, GL_DEPTH_COMPONENT, sm_width, sm_height,
      sm_cascade_count, GL_DEPTH_COMPONENT, GL_FLOAT);
  sm_depth_array_texture_->setParameter(GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  sm_depth_array_texture_->setParameter(GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  sm_depth_array_texture_->setParameter(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  sm_depth_array_texture_->setParameter(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  sm_depth_array_texture_->setParameter(GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
  sm_depth_array_texture_->setParameter(GL_TEXTURE_COMPARE_MODE,
                                        GL_COMPARE_R_TO_TEXTURE);
  sm_depth_array_texture_->setParameter(GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
  sm_framebuffer_ = std::make_shared<Framebuffer>();
}

void DirectionalLight::updateViewMatricesAndSplits(
    const Eigen::Matrix4f &camera_view_matrix, float aspect_ratio, float z_near,
    float z_far, float fov) {
  // Calculate cascade splits in view space
  for (int i = 0; i < sm_cascade_count_ + 1; ++i) {
    float t = static_cast<float>(i) / sm_cascade_count_;
    sm_cascade_splits_(i) = z_near * std::pow(z_far / z_near, t);
  }

  Eigen::Affine3f inv_camera_view_tf(camera_view_matrix.inverse());
  // https://lxjk.github.io/2017/04/15/Calculate-Minimal-Bounding-Sphere-of-Frustum.html
  float k =
      std::sqrt(1.0f + aspect_ratio * aspect_ratio) * std::tan(0.5f * fov);
  float k_sq = k * k;
  for (int i = 0; i < sm_cascade_count_; ++i) {
    float z_sn = sm_cascade_splits_(i);     // Near z of frustum segment
    float z_sf = sm_cascade_splits_(i + 1); // Far z of frustum segment
    float z_range = z_sf - z_sn;
    float z_sum = z_sf + z_sn;
    // Find a bounding sphere in view space
    Eigen::Vector3f center;
    float radius;
    if (k_sq >= z_range / z_sum) {
      center = Eigen::Vector3f{0.0f, 0.0f, -z_sf};
      radius = z_sf * k;
    } else {
      center = Eigen::Vector3f{0.0f, 0.0f, -0.5f * z_sum * (1.0f + k_sq)};
      radius = 0.5f * std::sqrt(z_range * z_range +
                                2.0f * (z_sf * z_sf + z_sn * z_sn) * k_sq +
                                z_sum * z_sum * k_sq * k_sq);
    }
    // Transform center of sphere into world space
    Eigen::Vector3f center_world = inv_camera_view_tf * center;
    view_matrices_.block<4, 4>(0, 4 * i) =
        Eigen::Affine3f(Eigen::Scaling(1.0f / radius) * view_rot_matrix_ *
                        Eigen::Translation3f(-center_world))
            .matrix();
  }
}

void ProgramState::updateUniforms(const Program &program) {
  if (proj_matrix_.dirty_) {
    program.setProjectionMatrix(proj_matrix_.value_);
  }
  if (view_matrix_.dirty_) {
    program.setViewMatrix(view_matrix_.value_);
  }
  if (tex_coords_matrix_.dirty_) {
    program.setTexCoordsMatrix(tex_coords_matrix_.value_);
  }
  if (view_matrix_.dirty_ || model_matrix_.dirty_) {
    Eigen::Matrix4f model_view_matrix =
        view_matrix_.value_ * model_matrix_.value_;
    program.setModelViewMatrix(model_view_matrix);
    program.setNormalMatrix(
        model_view_matrix.topLeftCorner<3, 3>().inverse().transpose());
  }
  if (proc_texture_type_.dirty_) {
    program.setProcTextureType(proc_texture_type_.value_);
  }
  if (object_color_.dirty_) {
    program.setObjectColor(object_color_.value_);
  }
  if (dir_light_dir_.dirty_) {
    program.setLightDir(dir_light_dir_.value_);
  }
  if (dir_light_proj_matrix_.dirty_) {
    program.setLightProjMatrix(dir_light_proj_matrix_.value_);
  }
  if (dir_light_color_.dirty_) {
    program.setLightColor(dir_light_color_.value_);
  }
  if (dir_light_view_matrices_.dirty_ || model_matrix_.dirty_) {
    Eigen::Matrix<float, 4, Eigen::Dynamic> light_mv_matrices(
        4, dir_light_view_matrices_.value_.cols());
    for (int j = 0; j < light_mv_matrices.cols(); j += 4) {
      light_mv_matrices.block<4, 4>(0, j) =
          dir_light_view_matrices_.value_.block<4, 4>(0, j) *
          model_matrix_.value_;
    }
    program.setLightModelViewMatrices(light_mv_matrices);
  }
  if (dir_light_sm_cascade_splits_.dirty_) {
    // Shader only supports up to 5 shadow map cascades at the moment
    // Only 4 splits are needed to describe 5 cascades, starting from index 1
    program.setCascadeFarSplits(
        dir_light_sm_cascade_splits_.value_.segment<4>(1));
  }

  proj_matrix_.dirty_ = false;
  view_matrix_.dirty_ = false;
  model_matrix_.dirty_ = false;
  proc_texture_type_.dirty_ = false;
  object_color_.dirty_ = false;
  dir_light_color_.dirty_ = false;
  dir_light_dir_.dirty_ = false;
  dir_light_proj_matrix_.dirty_ = false;
  dir_light_view_matrices_.dirty_ = false;
  dir_light_sm_cascade_splits_.dirty_ = false;
}

void makeOrthographicProjection(float aspect_ratio, float z_near, float z_far,
                                Ref<Eigen::Matrix4f> matrix) {
  float z_range = z_far - z_near;
  // clang-format off
  matrix << 1 / aspect_ratio, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, -2 / z_range, -(z_far + z_near) / z_range,
            0, 0, 0, 1;
  // clang-format on
}

void makePerspectiveProjection(float aspect_ratio, float z_near, float z_far,
                               float fov, Ref<Eigen::Matrix4f> matrix) {
  float z_range = z_far - z_near;
  float tan_half_fov = std::tan(0.5f * fov);
  // clang-format off
  matrix << 1 / (tan_half_fov * aspect_ratio), 0, 0, 0,
            0, 1 / tan_half_fov, 0, 0,
            0, 0, -(z_far + z_near) / z_range, -2 * z_far * z_near / z_range,
            0, 0, -1, 0;
  // clang-format on
}

std::shared_ptr<Mesh> makeBoxMesh() {
  // clang-format off
  std::vector<float> positions = {
      -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, // -X face
      -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // -Y face
      -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, // -Z face
      1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1,     // +X face
      1, 1, 1, 1, 1, -1, -1, 1, -1, -1, 1, 1,     // +Y face
      1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1};    // +Z face
  std::vector<float> normals = {
      -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0,     // -X face
      0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0,     // -Y face
      0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1,     // -Z face
      1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,         // +X face
      0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,         // +Y face
      0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1};        // +Z face
  std::vector<int> indices = {
      0, 1, 2, 3, 0, 2,                           // -X face
      4, 5, 6, 7, 4, 6,                           // -Y face
      8, 9, 10, 11, 8, 10,                        // -Z face
      12, 13, 14, 15, 12, 14,                     // +X face
      16, 17, 18, 19, 16, 18,                     // +Y face
      20, 21, 22, 23, 20, 22};                    // +Z face
  // clang-format on

  auto mesh = std::make_shared<Mesh>(GL_STATIC_DRAW);
  mesh->setPositions(positions);
  mesh->setNormals(normals);
  mesh->setIndices(indices);
  return mesh;
}

std::shared_ptr<Mesh> makeTubeMesh(int n_segments) {
  std::vector<float> positions;
  std::vector<float> normals;
  std::vector<int> indices;

  // Define two rings of vertices
  for (int i = 0; i < 2; ++i) {
    for (int j = 0; j < n_segments; ++j) {
      float theta = (2 * M_PI) * j / n_segments;
      float pos[3] = {(i == 0) ? -1.0f : 1.0f, std::cos(theta),
                      std::sin(theta)};
      float normal[3] = {0, std::cos(theta), std::sin(theta)};
      positions.insert(positions.end(), std::begin(pos), std::end(pos));
      normals.insert(normals.end(), std::begin(normal), std::end(normal));
    }
  }

  // Define triangles
  for (int j = 0; j < n_segments; ++j) {
    int idx_00 = j;
    int idx_01 = (j + 1) % n_segments;
    int idx_10 = n_segments + j;
    int idx_11 = n_segments + (j + 1) % n_segments;
    int idx[6] = {idx_00, idx_01, idx_10, idx_11, idx_10, idx_01};
    indices.insert(indices.end(), std::begin(idx), std::end(idx));
  }

  auto mesh = std::make_shared<Mesh>(GL_STATIC_DRAW);
  mesh->setPositions(positions);
  mesh->setNormals(normals);
  mesh->setIndices(indices);
  return mesh;
}

std::shared_ptr<Mesh> makeCapsuleEndMesh(int n_segments, int n_rings) {
  std::vector<float> positions;
  std::vector<int> indices;

  // Define rings of vertices
  for (int i = 0; i < n_rings; ++i) {
    for (int j = 0; j < n_segments; ++j) {
      float theta = (2 * M_PI) * j / n_segments;
      float phi = (M_PI / 2) * i / n_rings;
      float pos[3] = {std::sin(phi), std::cos(phi) * std::cos(theta),
                      std::cos(phi) * std::sin(theta)};
      positions.insert(positions.end(), std::begin(pos), std::end(pos));
    }
  }
  // Define zenith vertex
  float pos[3] = {1.0f, 0.0f, 0.0f};
  positions.insert(positions.end(), std::begin(pos), std::end(pos));

  // Define triangles for every ring except the last
  for (int i = 0; i < (n_rings - 1); ++i) {
    for (int j = 0; j < n_segments; ++j) {
      int idx_00 = i * n_segments + j;
      int idx_01 = i * n_segments + (j + 1) % n_segments;
      int idx_10 = (i + 1) * n_segments + j;
      int idx_11 = (i + 1) * n_segments + (j + 1) % n_segments;
      int idx[6] = {idx_00, idx_01, idx_10, idx_11, idx_10, idx_01};
      indices.insert(indices.end(), std::begin(idx), std::end(idx));
    }
  }
  // Define triangles for last ring
  for (int j = 0; j < n_segments; ++j) {
    int idx[3] = {(n_rings - 1) * n_segments + j,
                  (n_rings - 1) * n_segments + (j + 1) % n_segments,
                  n_rings * n_segments};
    indices.insert(indices.end(), std::begin(idx), std::end(idx));
  }

  // The positions and normals of points on a unit sphere are equal
  auto mesh = std::make_shared<Mesh>(GL_STATIC_DRAW);
  mesh->setPositions(positions);
  mesh->setNormals(positions);
  mesh->setIndices(indices);
  return mesh;
}

std::shared_ptr<Mesh> makeCylinderEndMesh(int n_segments) {
  std::vector<float> positions;
  std::vector<float> normals;
  std::vector<int> indices;

  // Define a ring of vertices
  for (int j = 0; j < n_segments; ++j) {
    float theta = (2 * M_PI) * j / n_segments;
    float pos[3] = {0.0f, std::cos(theta), std::sin(theta)};
    float normal[3] = {1.0f, 0.0f, 0.0f};
    positions.insert(positions.end(), std::begin(pos), std::end(pos));
    normals.insert(normals.end(), std::begin(normal), std::end(normal));
  }

  // Define a center vertex
  float pos[3] = {0.0f, 0.0f, 0.0f};
  float normal[3] = {1.0f, 0.0f, 0.0f};
  positions.insert(positions.end(), std::begin(pos), std::end(pos));
  normals.insert(normals.end(), std::begin(normal), std::end(normal));

  // Define triangles
  for (int j = 0; j < n_segments; ++j) {
    int idx[3] = {j, (j + 1) % n_segments, n_segments};
    indices.insert(indices.end(), std::begin(idx), std::end(idx));
  }

  auto mesh = std::make_shared<Mesh>(GL_STATIC_DRAW);
  mesh->setPositions(positions);
  mesh->setNormals(normals);
  mesh->setIndices(indices);
  return mesh;
}

std::shared_ptr<Texture2D> loadTexture(const std::string &path) {
  unsigned char *rgba_raw = nullptr;
  unsigned int width, height;
  unsigned int error =
      lodepng_decode32_file(&rgba_raw, &width, &height, path.c_str());
  std::unique_ptr<unsigned char[], decltype(std::free) *> rgba(rgba_raw,
                                                               std::free);
  if (error) {
    throw std::runtime_error("Could not load texture from file \"" + path +
                             "\": " + lodepng_error_text(error));
  }
  return std::make_shared<Texture2D>(
      /*target=*/GL_TEXTURE_2D, /*level=*/0, /*internal_format=*/GL_RGBA,
      /*width=*/width, /*height=*/height, /*format=*/GL_RGBA,
      /*type=*/GL_UNSIGNED_BYTE, /*data=*/rgba.get());
}

} // namespace robot_design


#include <cctbx/boost_python/flex_fwd.h>

#include <cctbx/xray/gradients_direct.h>
#include <boost/python/class.hpp>

namespace cctbx { namespace xray { namespace structure_factors {
namespace boost_python {

namespace {

  struct gradients_direct_wrappers
  {
    typedef gradients_direct<> w_t;
    typedef w_t::scatterer_type scatterer_type;
    typedef w_t::float_type float_type;

    static void
    wrap()
    {
      using namespace boost::python;
      class_<w_t>("structure_factors_gradients_direct", no_init)
        .def(init<uctbx::unit_cell const&,
                  sgtbx::space_group const&,
                  af::const_ref<miller::index<> > const&,
                  af::const_ref<scatterer_type> const&,
                  af::const_ref<float_type> const&,
                  scattering_type_registry const&,
                  sgtbx::site_symmetry_table const&,
                  af::const_ref<std::complex<float_type> > const&,
                  std::size_t>())
        .def(init<math::cos_sin_table<double> const&,
                  uctbx::unit_cell const&,
                  sgtbx::space_group const&,
                  af::const_ref<miller::index<> > const&,
                  af::const_ref<scatterer_type> const&,
                  af::const_ref<float_type> const&,
                  scattering_type_registry const&,
                  sgtbx::site_symmetry_table const&,
                  af::const_ref<std::complex<float_type> > const&,
                  std::size_t>())
        .def("packed", &w_t::packed)
        .def("d_target_d_site_frac", &w_t::d_target_d_site_frac)
        .def("d_target_d_u_iso", &w_t::d_target_d_u_iso)
        .def("d_target_d_u_star", &w_t::d_target_d_u_star)
        .def("d_target_d_occupancy", &w_t::d_target_d_occupancy)
        .def("d_target_d_fp", &w_t::d_target_d_fp)
        .def("d_target_d_fdp", &w_t::d_target_d_fdp)
      ;
    }
  };

} // namespace <anoymous>

}} // namespace structure_factors::boost_python

namespace boost_python {

  void wrap_gradients_direct()
  {
    structure_factors::boost_python::gradients_direct_wrappers::wrap();
  }

}}} // namespace cctbx::xray::boost_python


// Copyright (c) 2015-2018 Daniel Cooke
// Use of this source code is governed by the MIT license that can be found in the LICENSE file.

#ifndef haplotype_likelihood_model_hpp
#define haplotype_likelihood_model_hpp

#include <vector>
#include <iterator>
#include <cstddef>
#include <cstdint>
#include <algorithm>
#include <functional>
#include <memory>
#include <stdexcept>

#include <boost/optional.hpp>

#include "config/common.hpp"
#include "basics/contig_region.hpp"
#include "basics/cigar_string.hpp"
#include "basics/aligned_read.hpp"
#include "core/types/haplotype.hpp"
#include "core/models/error/snv_error_model.hpp"
#include "core/models/error/indel_error_model.hpp"
#include "pairhmm/pair_hmm.hpp"

#include "timers.hpp"

namespace octopus {

class HaplotypeLikelihoodModel
{
public:
    using Penalty = hmm::MutationModel::Penalty;

    struct Config
    {
        bool use_mapping_quality = true;
        boost::optional<AlignedRead::MappingQuality> mapping_quality_cap_trigger = boost::none;
        AlignedRead::MappingQuality mapping_quality_cap = 120;
        bool use_flank_state = true;
    };

    struct FlankState
    {
        ContigRegion::Position lhs_flank, rhs_flank;
    };

    class ShortHaplotypeError;

    using MappingPosition       = std::size_t;
    using MappingPositionVector = std::vector<MappingPosition>;
    using MappingPositionItr    = MappingPositionVector::const_iterator;

    struct Alignment
    {
        MappingPosition mapping_position;
        CigarString cigar;
        double likelihood;
    };

    HaplotypeLikelihoodModel();
    HaplotypeLikelihoodModel(Config config);
    HaplotypeLikelihoodModel(std::unique_ptr<SnvErrorModel> snv_model,
                             std::unique_ptr<IndelErrorModel> indel_model);
    HaplotypeLikelihoodModel(std::unique_ptr<SnvErrorModel> snv_model,
                             std::unique_ptr<IndelErrorModel> indel_model,
                             Config config);

    HaplotypeLikelihoodModel(const HaplotypeLikelihoodModel&);
    HaplotypeLikelihoodModel& operator=(const HaplotypeLikelihoodModel&);
    HaplotypeLikelihoodModel(HaplotypeLikelihoodModel&&)            = default;
    HaplotypeLikelihoodModel& operator=(HaplotypeLikelihoodModel&&) = default;

    friend void swap(HaplotypeLikelihoodModel& lhs, HaplotypeLikelihoodModel& rhs) noexcept;

    ~HaplotypeLikelihoodModel() = default;

    static unsigned pad_requirement() noexcept;

    bool can_use_flank_state() const noexcept;

    void reset(const Haplotype& haplotype, boost::optional<FlankState> flank_state = boost::none);

    void clear() noexcept;

    // ln p(read | haplotype, model)
    double evaluate(const AlignedRead& read) const;
    double evaluate(const AlignedRead& read, const MappingPositionVector& mapping_positions) const;
    double evaluate(const AlignedRead& read, MappingPositionItr first_mapping_position, MappingPositionItr last_mapping_position) const;

    Alignment align(const AlignedRead& read) const;
    Alignment align(const AlignedRead& read, const MappingPositionVector& mapping_positions) const;
    Alignment align(const AlignedRead& read, MappingPositionItr first_mapping_position, MappingPositionItr last_mapping_position) const;

private:
    std::unique_ptr<SnvErrorModel> snv_error_model_;
    std::unique_ptr<IndelErrorModel> indel_error_model_;

    const Haplotype* haplotype_;

    boost::optional<FlankState> haplotype_flank_state_;

    std::vector<char> haplotype_snv_forward_mask_, haplotype_snv_reverse_mask_;
    std::vector<Penalty> haplotype_snv_forward_priors_, haplotype_snv_reverse_priors_;

    std::vector<Penalty> haplotype_gap_open_penalities_;
    Penalty haplotype_gap_extension_penalty_;
    Config config_;
};

class HaplotypeLikelihoodModel::ShortHaplotypeError : public std::runtime_error
{
public:
    using Length = Haplotype::NucleotideSequence::size_type;

    ShortHaplotypeError() = delete;

    ShortHaplotypeError(const Haplotype& haplotype, Length required_extension);

    const Haplotype& haplotype() const noexcept;

    Length required_extension() const noexcept;

private:
    const Haplotype& haplotype_;
    Length required_extension_;
};

HaplotypeLikelihoodModel make_haplotype_likelihood_model(const std::string sequencer, bool use_mapping_quality = true);

} // namespace octopus

#endif


/* Output File Format:
 * 1st int: Number of classes
 * 2nd int: output feature vector length N
 * Follow P (unknown) patches:
 *      int object_id,
 *      float yaw, pitch, roll
 *      float x, y, z (in object coordinates)
 *      float[N] output vector
 */

#include <iostream>

#include <train_patch_generator.h>
#include <glog/logging.h>
#include <lmdb.h>
#include <boost/algorithm/string.hpp>

#include <cv.h>
#include <highgui.h>
#include <fstream>

void train_patch_generator::generate_train_patches(){

    //check caffe files
    CHECK_GT(caffe_model_definition_filename_.size(), 0) << "No caffe definition model defined.";
    CHECK_GT(caffe_model_weights_filename_.size(), 0) << "No caffe weights model defined.";

    //caffe::Caffe::set_phase(caffe::Caffe::TEST);  // used with previous version of Caffe
    caffe::Net<float> caffe_net(caffe_model_definition_filename_, caffe::TEST);
    caffe_net.CopyTrainedLayersFrom(caffe_model_weights_filename_);

    //create lmdb database
    MDB_env* mdb_env;
    MDB_dbi mdb_dbi;
    MDB_txn* mdb_txn;
    MDB_cursor* mdb_cursor;
    MDB_val mdb_key, mdb_value;
    CHECK_GT(input_lmdb_.size(), 0) << "No lmdb input specified.";
    CHECK_EQ(mdb_env_create(&mdb_env), MDB_SUCCESS) << "mdb_env_create failed";
    CHECK_EQ(mdb_env_set_mapsize(mdb_env, 1099511627776), MDB_SUCCESS);  // 1TB
    CHECK_EQ(mdb_env_open(mdb_env,
             input_lmdb_.c_str(),
             MDB_RDONLY|MDB_NOTLS, 0664), MDB_SUCCESS) << "mdb_env_open failed";
    CHECK_EQ(mdb_txn_begin(mdb_env, NULL, MDB_RDONLY, &mdb_txn), MDB_SUCCESS)
        << "mdb_txn_begin failed";
    CHECK_EQ(mdb_open(mdb_txn, NULL, 0, &mdb_dbi), MDB_SUCCESS)
        << "mdb_open failed";
    CHECK_EQ(mdb_cursor_open(mdb_txn, mdb_dbi, &mdb_cursor), MDB_SUCCESS)
        << "mdb_cursor_open failed";
    CHECK_EQ(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_FIRST),
        MDB_SUCCESS) << "mdb_cursor_get failed";

    //annotation file
    std::ifstream fannot((input_lmdb_ + "/patch_annotation_lmdb.txt").c_str());
    CHECK(fannot) << "Cannot open annotation file: " << input_lmdb_ + "/patch_annotation_lmdb.txt";

    CHECK(output_file_.size() > 0) << "No output file specified";

    //output file
    std::ofstream fout(output_file_.c_str(), std::ios::out | std::ios::binary);
    CHECK(fout) << "Could not open output file " << output_file_ << " for writing.";

    //Get number of classes
    int num_classes;
    fannot >> num_classes;
    //write number of classes
    fout.write((char*)&num_classes, sizeof(int));

    bool output_vector_length_written = false;
    int num_patches = 0;
    while(true){

        bool data_end = false;
        std::vector<Annotation> annot_vec;
        std::vector<float> float_data;
        for(int b=0; b<batch_size_; ++b){

            //get patch from lmdb
            caffe::Datum datum;
            datum.ParseFromArray(mdb_value.mv_data, mdb_value.mv_size);
            std::string key((char*)mdb_key.mv_data);
            key.resize(13); //format: xxxx_yyyyyyyy -> x: obj number, y: patch number

            //get annotation
            Annotation annot;
            CHECK(fannot >> annot.annot_key >> annot.yaw >> annot.pitch >> annot.roll >> annot.obj_x >> annot.obj_y >> annot.obj_z)
                    << "Couldn't read annotation. Maybe not enough entries?";

            CHECK_EQ(key, annot.annot_key) << "annotation and database key mismatch";

            annot_vec.push_back(annot);

            //normalize & copy patch to Net
            //std::vector<float> float_data((unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
            int start = float_data.size();
            float_data.insert(float_data.end(), (unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
            //apply same scale to the input as in training
            for(int i=start; i<float_data.size(); ++i)
                float_data[i] /= 255.0f;

            if(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_NEXT) != MDB_SUCCESS){
                data_end = true;
                break;
            }

        }

        if(data_end)
            break;

        if(use_gpu_)
            caffe::caffe_copy(caffe_net.input_blobs()[0]->count(), &(float_data[0]), caffe_net.input_blobs()[0]->mutable_gpu_data());
        else
            caffe::caffe_copy(caffe_net.input_blobs()[0]->count(), &(float_data[0]), caffe_net.input_blobs()[0]->mutable_cpu_data());

        //Get feature from Net - forward image patch
        //output_blob[0]: contains the result
        //output_blob[1]: contains the input
        const std::vector<caffe::Blob<float>* > output_blob = caffe_net.ForwardPrefilled();

        for(int b=0; b<batch_size_; ++b){

            int feature_vector_length = output_blob[0]->count() / batch_size_;

            //write vector length only once
            if(!output_vector_length_written){
                int output_vector_length = feature_vector_length;
                fout.write((char*)&output_vector_length, sizeof(int));
                output_vector_length_written = true;
            }

            //write annotation & feature vector
            std::vector<std::string> object_id_str;
            boost::split(object_id_str, annot_vec[b].annot_key, boost::is_any_of("_"));
            int obj_id = boost::lexical_cast<int>(object_id_str[0].c_str(), object_id_str[0].size());

            fout.write((char*)&obj_id,  sizeof(int));
            fout.write((char*)&annot_vec[b].yaw,     sizeof(float));
            fout.write((char*)&annot_vec[b].pitch,   sizeof(float));
            fout.write((char*)&annot_vec[b].roll,    sizeof(float));
            fout.write((char*)&annot_vec[b].obj_x,   sizeof(float));
            fout.write((char*)&annot_vec[b].obj_y,   sizeof(float));
            fout.write((char*)&annot_vec[b].obj_z,   sizeof(float));

            for(int i=0; i<feature_vector_length; ++i){
                float f = output_blob[0]->data_at(b, i, 0, 0);
                fout.write((char*)&f, sizeof(float));
            }

//    //      visualize patches and output, works only with full network, i.e. full reconstruction
//            cv::Mat rgb1(patch_size_, patch_size_, CV_8UC3);
//            cv::Mat rgb2(patch_size_, patch_size_, CV_8UC3);
//            for(int c=0; c<3; ++c){
//                for(int h=0; h<patch_size_; ++h){
//                    for(int w=0; w<patch_size_; ++w){
//                        int idx = c*patch_size_*patch_size_ + h*patch_size_ + w;
//                        rgb1.at<cv::Vec3b>(h, w)[c] = static_cast<unsigned char>(output_blob[0]->data_at(b, idx, 0, 0) * 255.0f);
//                        rgb2.at<cv::Vec3b>(h, w)[c] = static_cast<unsigned char>(output_blob[1]->data_at(b, idx, 0, 0) * 255.0f);
//                    }
//                }
//            }

//            cv::resize(rgb1, rgb1, cv::Size(100, 100));
//            cv::resize(rgb2, rgb2, cv::Size(100, 100));
//            int k=-1;
//            while(k==-1){
//                cv::imshow("rgb1", rgb1);
//                cv::imshow("rgb2", rgb2);
//                k = cv::waitKey(30);
//            }

        }

        num_patches += batch_size_;
        std::cout << "Training samples written: " << num_patches << "\r";

    }
    std::cout << std::endl << "Finished!" << std::endl;

    //close lmdb database
    mdb_cursor_close(mdb_cursor);
    mdb_close(mdb_env, mdb_dbi);
    mdb_txn_abort(mdb_txn);
    mdb_env_close(mdb_env);

    //close files
    fannot.close();
    fout.close();

}

void train_patch_generator::generate_train_patches_pixeltests(){

    //create lmdb database
    MDB_env* mdb_env;
    MDB_dbi mdb_dbi;
    MDB_txn* mdb_txn;
    MDB_cursor* mdb_cursor;
    MDB_val mdb_key, mdb_value;
    CHECK_GT(input_lmdb_.size(), 0) << "No lmdb input specified.";
    CHECK_EQ(mdb_env_create(&mdb_env), MDB_SUCCESS) << "mdb_env_create failed";
    CHECK_EQ(mdb_env_set_mapsize(mdb_env, 1099511627776), MDB_SUCCESS);  // 1TB
    CHECK_EQ(mdb_env_open(mdb_env,
             input_lmdb_.c_str(),
             MDB_RDONLY|MDB_NOTLS, 0664), MDB_SUCCESS) << "mdb_env_open failed";
    CHECK_EQ(mdb_txn_begin(mdb_env, NULL, MDB_RDONLY, &mdb_txn), MDB_SUCCESS)
        << "mdb_txn_begin failed";
    CHECK_EQ(mdb_open(mdb_txn, NULL, 0, &mdb_dbi), MDB_SUCCESS)
        << "mdb_open failed";
    CHECK_EQ(mdb_cursor_open(mdb_txn, mdb_dbi, &mdb_cursor), MDB_SUCCESS)
        << "mdb_cursor_open failed";
    CHECK_EQ(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_FIRST),
        MDB_SUCCESS) << "mdb_cursor_get failed";

    //annotation file
    std::ifstream fannot((input_lmdb_ + "/patch_annotation_lmdb.txt").c_str());
    CHECK(fannot) << "Cannot open annotation file: " << input_lmdb_ + "/patch_annotation_lmdb.txt";

    CHECK(output_file_.size() > 0) << "No output file specified";

    //output file
    std::ofstream fout(output_file_.c_str(), std::ios::out | std::ios::binary);
    CHECK(fout) << "Could not open output file " << output_file_ << " for writing.";

    //Get number of classes
    int num_classes;
    fannot >> num_classes;
    //write number of classes
    fout.write((char*)&num_classes, sizeof(int));

    bool output_vector_length_written = false;
    int num_patches = 0;

    int feature_vector_length = 0;

    while(true){

        bool data_end = false;
        std::vector<Annotation> annot_vec;
        std::vector<float> float_data;
        for(int b=0; b<batch_size_; ++b){

            //get patch from lmdb
            caffe::Datum datum;
            datum.ParseFromArray(mdb_value.mv_data, mdb_value.mv_size);
            std::string key((char*)mdb_key.mv_data);
            key.resize(13); //format: xxxx_yyyyyyyy -> x: obj number, y: patch number

            //get annotation
            Annotation annot;
            CHECK(fannot >> annot.annot_key >> annot.yaw >> annot.pitch >> annot.roll >> annot.obj_x >> annot.obj_y >> annot.obj_z)
                    << "Couldn't read annotation. Maybe not enough entries?";

            CHECK_EQ(key, annot.annot_key) << "annotation and database key mismatch";

            annot_vec.push_back(annot);

            //normalize & copy patch to Net
            //std::vector<float> float_data((unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
            int start = float_data.size();
            float_data.insert(float_data.end(), (unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
            //apply same scale to the input as in training
            for(int i=start; i<float_data.size(); ++i)
                float_data[i] /= 255.0f;

            if(feature_vector_length == 0)
                feature_vector_length = datum.data().size();

            if(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_NEXT) != MDB_SUCCESS){
                data_end = true;
                break;
            }

        }

        if(data_end)
            break;

        for(int b=0; b<batch_size_; ++b){

            //write vector length only once
            if(!output_vector_length_written){
                int output_vector_length = feature_vector_length;
                fout.write((char*)&output_vector_length, sizeof(int));
                output_vector_length_written = true;
            }

            //write annotation & feature vector
            std::vector<std::string> object_id_str;
            boost::split(object_id_str, annot_vec[b].annot_key, boost::is_any_of("_"));
            int obj_id = boost::lexical_cast<int>(object_id_str[0].c_str(), object_id_str[0].size());

            fout.write((char*)&obj_id,  sizeof(int));
            fout.write((char*)&annot_vec[b].yaw,     sizeof(float));
            fout.write((char*)&annot_vec[b].pitch,   sizeof(float));
            fout.write((char*)&annot_vec[b].roll,    sizeof(float));
            fout.write((char*)&annot_vec[b].obj_x,   sizeof(float));
            fout.write((char*)&annot_vec[b].obj_y,   sizeof(float));
            fout.write((char*)&annot_vec[b].obj_z,   sizeof(float));

            fout.write((char*)&float_data[b*feature_vector_length], feature_vector_length * sizeof(float));

        }

        num_patches += batch_size_;
        std::cout << "Training samples written: " << num_patches << "\r";

    }
    std::cout << std::endl << "Finished!" << std::endl;

    //close lmdb database
    mdb_cursor_close(mdb_cursor);
    mdb_close(mdb_env, mdb_dbi);
    mdb_txn_abort(mdb_txn);
    mdb_env_close(mdb_env);

    //close files
    fannot.close();
    fout.close();

}

void train_patch_generator::generate_train_patches_kmeans_centers(){

    //create lmdb database
    MDB_env* mdb_env;
    MDB_dbi mdb_dbi;
    MDB_txn* mdb_txn;
    MDB_cursor* mdb_cursor;
    MDB_val mdb_key, mdb_value;
    CHECK_GT(input_lmdb_.size(), 0) << "No lmdb input specified.";
    CHECK_EQ(mdb_env_create(&mdb_env), MDB_SUCCESS) << "mdb_env_create failed";
    CHECK_EQ(mdb_env_set_mapsize(mdb_env, 1099511627776), MDB_SUCCESS);  // 1TB
    CHECK_EQ(mdb_env_open(mdb_env,
             input_lmdb_.c_str(),
             MDB_RDONLY|MDB_NOTLS, 0664), MDB_SUCCESS) << "mdb_env_open failed";
    CHECK_EQ(mdb_txn_begin(mdb_env, NULL, MDB_RDONLY, &mdb_txn), MDB_SUCCESS)
        << "mdb_txn_begin failed";
    CHECK_EQ(mdb_open(mdb_txn, NULL, 0, &mdb_dbi), MDB_SUCCESS)
        << "mdb_open failed";
    CHECK_EQ(mdb_cursor_open(mdb_txn, mdb_dbi, &mdb_cursor), MDB_SUCCESS)
        << "mdb_cursor_open failed";
    CHECK_EQ(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_FIRST),
        MDB_SUCCESS) << "mdb_cursor_get failed";

    //annotation file
    std::ifstream fannot((input_lmdb_ + "/patch_annotation_lmdb.txt").c_str());
    CHECK(fannot) << "Cannot open annotation file: " << input_lmdb_ + "/patch_annotation_lmdb.txt";

    CHECK(output_file_.size() > 0) << "No output file specified";

    //Get number of classes
    int num_classes;
    fannot >> num_classes;

    int num_patches = 0;

    int feature_vector_length = 0;

    std::vector<float> float_data;
    std::vector<Annotation> annot_vec;

    while(true){

        bool data_end = false;

        //get patch from lmdb
        caffe::Datum datum;
        datum.ParseFromArray(mdb_value.mv_data, mdb_value.mv_size);
        std::string key((char*)mdb_key.mv_data);
        key.resize(13); //format: xxxx_yyyyyyyy -> x: obj number, y: patch number

        //get annotation
        Annotation annot;
        CHECK(fannot >> annot.annot_key >> annot.yaw >> annot.pitch >> annot.roll >> annot.obj_x >> annot.obj_y >> annot.obj_z)
                << "Couldn't read annotation. Maybe not enough entries?";

        CHECK_EQ(key, annot.annot_key) << "annotation and database key mismatch";

        annot_vec.push_back(annot);

        //normalize & copy patch to Net
        //std::vector<float> float_data((unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
        int start = float_data.size();
        float_data.insert(float_data.end(), (unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
        //apply same scale to the input as in training
        for(int i=start; i<float_data.size(); ++i)
            float_data[i] /= 255.0f;

        if(feature_vector_length == 0)
            feature_vector_length = datum.data().size();

        if(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_NEXT) != MDB_SUCCESS){
            data_end = true;
            break;
        }

        if(data_end)
            break;

    }

    int nvectors = float_data.size() / feature_vector_length;
    cv::Mat data(nvectors, feature_vector_length, CV_32FC1);
    std::cout << "nvectors: " << nvectors << std::endl;

    //nvectors = 800;
    int nfeatures = 400;

    for(int row = 0; row < nvectors; ++row)
        for(int col = 0; col < feature_vector_length; ++col)
            data.at<float>(row, col) = float_data[row*feature_vector_length + col];

    cv::Mat bestLabels;
    cv::TermCriteria criteria(cv::TermCriteria::COUNT, 100, 0.001);
    cv::Mat centers;
    cv::kmeans(data, nfeatures, bestLabels, criteria, 1, cv::KMEANS_PP_CENTERS, centers);

    cv::FileStorage fout_centers(output_file_ + ".centers.xml", cv::FileStorage::WRITE);
    fout_centers << "centers" << centers;
    fout_centers.release();

    std::cout << std::endl << "Finished!" << std::endl;

    //close lmdb database
    mdb_cursor_close(mdb_cursor);
    mdb_close(mdb_env, mdb_dbi);
    mdb_txn_abort(mdb_txn);
    mdb_env_close(mdb_env);

    //close files
    fannot.close();

}

void train_patch_generator::generate_train_patches_kmeans_vectors(){

    //create lmdb database
    MDB_env* mdb_env;
    MDB_dbi mdb_dbi;
    MDB_txn* mdb_txn;
    MDB_cursor* mdb_cursor;
    MDB_val mdb_key, mdb_value;
    CHECK_GT(input_lmdb_.size(), 0) << "No lmdb input specified.";
    CHECK_EQ(mdb_env_create(&mdb_env), MDB_SUCCESS) << "mdb_env_create failed";
    CHECK_EQ(mdb_env_set_mapsize(mdb_env, 1099511627776), MDB_SUCCESS);  // 1TB
    CHECK_EQ(mdb_env_open(mdb_env,
             input_lmdb_.c_str(),
             MDB_RDONLY|MDB_NOTLS, 0664), MDB_SUCCESS) << "mdb_env_open failed";
    CHECK_EQ(mdb_txn_begin(mdb_env, NULL, MDB_RDONLY, &mdb_txn), MDB_SUCCESS)
        << "mdb_txn_begin failed";
    CHECK_EQ(mdb_open(mdb_txn, NULL, 0, &mdb_dbi), MDB_SUCCESS)
        << "mdb_open failed";
    CHECK_EQ(mdb_cursor_open(mdb_txn, mdb_dbi, &mdb_cursor), MDB_SUCCESS)
        << "mdb_cursor_open failed";
    CHECK_EQ(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_FIRST),
        MDB_SUCCESS) << "mdb_cursor_get failed";

    //annotation file
    std::ifstream fannot((input_lmdb_ + "/patch_annotation_lmdb.txt").c_str());
    CHECK(fannot) << "Cannot open annotation file: " << input_lmdb_ + "/patch_annotation_lmdb.txt";

    CHECK(output_file_.size() > 0) << "No output file specified";

    //output file
    std::ofstream fout(output_file_.c_str(), std::ios::out | std::ios::binary);
    CHECK(fout) << "Could not open output file " << output_file_ << " for writing.";

    //Get number of classes
    int num_classes;
    fannot >> num_classes;
    //write number of classes
    fout.write((char*)&num_classes, sizeof(int));

    int num_patches = 0;

    int feature_vector_length = 0;

    std::vector<float> float_data;
    std::vector<Annotation> annot_vec;

    while(true){

        bool data_end = false;

        //get patch from lmdb
        caffe::Datum datum;
        datum.ParseFromArray(mdb_value.mv_data, mdb_value.mv_size);
        std::string key((char*)mdb_key.mv_data);
        key.resize(13); //format: xxxx_yyyyyyyy -> x: obj number, y: patch number

        //get annotation
        Annotation annot;
        CHECK(fannot >> annot.annot_key >> annot.yaw >> annot.pitch >> annot.roll >> annot.obj_x >> annot.obj_y >> annot.obj_z)
                << "Couldn't read annotation. Maybe not enough entries?";

        CHECK_EQ(key, annot.annot_key) << "annotation and database key mismatch";

        annot_vec.push_back(annot);

        //normalize & copy patch to Net
        //std::vector<float> float_data((unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
        int start = float_data.size();
        float_data.insert(float_data.end(), (unsigned char*)&datum.data()[0], (unsigned char*)&datum.data()[0] + datum.data().size());
        //apply same scale to the input as in training
        for(int i=start; i<float_data.size(); ++i)
            float_data[i] /= 255.0f;

        if(feature_vector_length == 0)
            feature_vector_length = datum.data().size();

        if(mdb_cursor_get(mdb_cursor, &mdb_key, &mdb_value, MDB_NEXT) != MDB_SUCCESS){
            data_end = true;
            break;
        }

        if(data_end)
            break;

    }

    int nvectors = float_data.size() / feature_vector_length;
    cv::Mat data(nvectors, feature_vector_length, CV_32FC1);
    std::cout << "nvectors: " << nvectors << std::endl;

    //nvectors = 800;
    int nfeatures = 400;

    for(int row = 0; row < nvectors; ++row)
        for(int col = 0; col < feature_vector_length; ++col)
            data.at<float>(row, col) = float_data[row*feature_vector_length + col];

    cv::Mat centers;
    cv::FileStorage fout_centers(output_file_ + ".centers.xml", cv::FileStorage::READ);
    fout_centers["centers"] >> centers;
    fout_centers.release();

    fout.write((char*)&nfeatures, sizeof(int));

    for(int b=0; b<nvectors; ++b){
        //write annotation & feature vector
        std::vector<std::string> object_id_str;
        boost::split(object_id_str, annot_vec[b].annot_key, boost::is_any_of("_"));
        int obj_id = boost::lexical_cast<int>(object_id_str[0].c_str(), object_id_str[0].size());

        fout.write((char*)&obj_id,  sizeof(int));
        fout.write((char*)&annot_vec[b].yaw,     sizeof(float));
        fout.write((char*)&annot_vec[b].pitch,   sizeof(float));
        fout.write((char*)&annot_vec[b].roll,    sizeof(float));
        fout.write((char*)&annot_vec[b].obj_x,   sizeof(float));
        fout.write((char*)&annot_vec[b].obj_y,   sizeof(float));
        fout.write((char*)&annot_vec[b].obj_z,   sizeof(float));

        std::vector<float> z(nfeatures);
        float meanz = 0;
        for(int k=0; k<nfeatures; ++k){
            z[k] = 0;
            for(int i=0; i<feature_vector_length; ++i)
                z[k] += pow(data.at<float>(b, i) - centers.at<float>(k, i), 2);
            z[k] = sqrt(z[k]);
            meanz += z[k] / nfeatures;
        }

        std::vector<float> fvec(nfeatures, 0);
        for(int k=0; k<nfeatures; ++k)
            fvec[k] = std::max((float)0, float(meanz - z[k]));

        fout.write((char*)&(fvec[0]), nfeatures * sizeof(float));

        num_patches++;
        std::cout << "Training samples written: " << num_patches << "\r";
    }

    std::cout << std::endl << "Finished!" << std::endl;

    //close lmdb database
    mdb_cursor_close(mdb_cursor);
    mdb_close(mdb_env, mdb_dbi);
    mdb_txn_abort(mdb_txn);
    mdb_env_close(mdb_env);

    //close files
    fannot.close();
    fout.close();

}


//
// Created by krab1k on 6.11.18.
//

#include <vector>
#include <map>
#include <set>
#include <dlfcn.h>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <Eigen/Dense>
#include <omp.h>

#include "chargefw2.h"
#include "method.h"
#include "parameters.h"
#include "utility/strings.h"

std::vector<RequiredFeatures> Method::get_requirements() const {
    return {};
}

void Method::set_parameters(Parameters *parameters) {
    if (common_parameters_.size() + atom_parameters_.size() + bond_parameters_.size() == 0 and parameters == nullptr) {
        return;
    }
    if (parameters->common() != nullptr and parameters->common()->names() != common_parameters_) {
        fmt::print(stderr, "Parameters don't match\n");
        fmt::print(stderr, "Expected: {}\n", common_parameters_);
        fmt::print(stderr, "Got: {}\n", parameters->common()->names());
        throw std::runtime_error("Invalid common parameters provided");
    }

    if (parameters->atom() != nullptr and parameters->atom()->names() != atom_parameters_) {
        fmt::print(stderr, "Parameters don't match\n");
        fmt::print(stderr, "Expected: {}\n", atom_parameters_);
        fmt::print(stderr, "Got: {}\n", parameters->atom()->names());
        throw std::runtime_error("Invalid atom parameters provided");
    }

    if (parameters->bond() != nullptr and parameters->bond()->names() != bond_parameters_) {
        fmt::print(stderr, "Parameters don't match\n");
        fmt::print(stderr, "Expected: {}\n", bond_parameters_);
        fmt::print(stderr, "Got: {}\n", parameters->bond()->names());
        throw std::runtime_error("Invalid bond parameters provided");
    }
    parameters_ = parameters;
}

template<>
std::string Method::get_option_value<std::string>(const std::string &name) const {
    return option_values_.at(name);
}

bool Method::is_suitable_for_molecule(const Molecule &) const {
    return true;
}

std::string Method::internal_name() const {
    auto name = to_lowercase(name_);
    name.erase(std::remove_if(name.begin(), name.end(), [](char c) { return !std::isalnum(c); }), name.end());
    return name;
}

template<>
double Method::get_option_value<double>(const std::string &name) const {
    return std::stod(option_values_.at(name));
}

template<>
int Method::get_option_value<int>(const std::string &name) const {
    return std::stoi(option_values_.at(name));
}

bool EEMethod::is_suitable_for_large_molecule() const {
    return true;
}

Eigen::VectorXd EEMethod::solve_EE(const Molecule &molecule,
        const std::function<Eigen::VectorXd(const std::vector<const Atom *> &, double)> &EE_function) const {

    auto method = get_option_value<std::string>("type");
    auto radius = get_option_value<double>("radius");

    if (method != "cover" and molecule.atoms().size() > 80000) {
        fmt::print("Switching to cover as the molecule is too big\n");
        fmt::print("Using radius {}\n", radius);
        method = "cover";
    } else if (method == "full" and molecule.atoms().size() > 20000) {
        fmt::print("Switching to cutoff as the molecule is too big\n");
        fmt::print("Using radius {}\n", radius);
        method = "cutoff";
    }

    if (method == "full") {
        Eigen::setNbThreads(0);
        std::vector<const Atom *> fragment_atoms;
        for (const auto &atom: molecule.atoms()) {
            fragment_atoms.push_back(&atom);
        }

        return EE_function(fragment_atoms, molecule.total_charge());

    } else if (method == "cutoff") {
        const size_t n = molecule.atoms().size();
        Eigen::VectorXd results = Eigen::VectorXd::Zero(n);
        Eigen::setNbThreads(1);

#pragma omp parallel for default(none) shared(results, radius, molecule, EE_function) firstprivate(n)
        for (size_t i = 0; i < n; i++) {
            auto fragment_atoms = molecule.get_close_atoms(molecule.atoms()[i], radius);
            Eigen::VectorXd res = EE_function(fragment_atoms,
                                    static_cast<double>(molecule.total_charge()) * fragment_atoms.size() /
                                    molecule.atoms().size());
            results(i) = res(0);
        }

        double correction = molecule.total_charge() - results.sum();
        correction /= molecule.atoms().size();

        results.array() += correction;
        return results;

    } else /* method == "cover" */ {
        Eigen::setNbThreads(1);

        const size_t n = molecule.atoms().size();

        /* 1st step - identify pivots */
        std::map<size_t, std::set<size_t>> neighbors;
        for (const auto &bond: molecule.bonds()) {
            neighbors[bond.first().index()].insert(bond.second().index());
            neighbors[bond.second().index()].insert(bond.first().index());
        }

        std::set<size_t> all;
        for (size_t i = 0; i < n; i++) {
            all.insert(i);
        }

        std::map<size_t, std::set<size_t>> bonding_sizes;
        for (const auto &[key, val]: neighbors) {
            bonding_sizes[val.size()].insert(key);
        }

        std::set<const Atom *> pivots;
        for (auto it = bonding_sizes.rbegin(); it != bonding_sizes.rend(); it++) {
            for (const auto &idx: it->second) {
                if (all.find(idx) != all.end()) {
                    pivots.insert(&molecule.atoms()[idx]);
                    for (const auto &neighbor: neighbors[idx]) {
                        all.erase(neighbor);
                    }
                }
            }
        }

        /* 2nd step - solve EEM for fragments, sum up charges */
        Eigen::VectorXd results = Eigen::VectorXd::Zero(n);
        std::vector<int> charges_count(n, 0);

        std::vector<const Atom *> pivots_vector(pivots.begin(), pivots.end());

#pragma omp parallel for default(none) shared(radius, pivots_vector, neighbors, molecule, charges_count, results, EE_function) firstprivate(n)
        for (size_t i = 0; i < pivots_vector.size(); i++) {
            auto &atom = pivots_vector[i];
            auto fragment_atoms = molecule.get_close_atoms(*atom, radius);
            Eigen::VectorXd res = EE_function(fragment_atoms,
                                    static_cast<double>(molecule.total_charge()) * fragment_atoms.size() / n);

            std::set<size_t> close_atoms = {atom->index()};
            for (const auto &j: neighbors[atom->index()]) {
                close_atoms.insert(j);
                for (const auto &k: neighbors[j]) {
                    close_atoms.insert(k);
                }
            }

            for (const auto &j: close_atoms) {
#pragma omp atomic
                charges_count[j]++;
            }

            for (size_t j = 0; j < fragment_atoms.size(); j++) {
                if (close_atoms.find(fragment_atoms[j]->index()) != close_atoms.end()) {
#pragma omp atomic
                    results(fragment_atoms[j]->index()) += res(j);
                }
            }
        }

        for (long i = 0; i < results.size(); i++) {
            results(i) /= charges_count[i];
        }

        /* 3rd step - correct charges */
        auto correction = (molecule.total_charge() - results.sum()) / n;
        results.array() += correction;
        return results;
    }
}

Method* load_method(const std::string &method_name) {

    std::string file;
    if (ends_with(method_name, ".so")) {
        file = method_name;
    } else {
        file = (std::string(INSTALL_DIR) + "/lib/lib" + method_name + ".so");
    }

    auto handle = dlopen(file.c_str(), RTLD_LAZY);

    auto get_method_handle = (Method *(*)())(dlsym(handle, "get_method"));
    if (!get_method_handle) {
        fmt::print(stderr, "{}\n", dlerror());
        exit(EXIT_FILE_ERROR);
    }

    return (*get_method_handle)();
}


#include "JoinSplit.hpp"
#include "prf.h"
#include "sodium.h"

#include "arnak/util.h"

#include <memory>

#include <boost/foreach.hpp>
#include <boost/format.hpp>
#include <boost/optional.hpp>
#include <fstream>
#include "tinyformat.h"
#include "sync.h"
#include "amount.h"

#include "librustzcash.h"
#include "streams.h"
#include "version.h"

namespace libzcash {

static CCriticalSection cs_ParamsIO;

template<size_t NumInputs, size_t NumOutputs>
class JoinSplitCircuit : public JoinSplit<NumInputs, NumOutputs> {
public:
    JoinSplitCircuit() {}
    ~JoinSplitCircuit() {}

    SproutProof prove(
        const std::array<JSInput, NumInputs>& inputs,
        const std::array<JSOutput, NumOutputs>& outputs,
        std::array<SproutNote, NumOutputs>& out_notes,
        std::array<ZCNoteEncryption::Ciphertext, NumOutputs>& out_ciphertexts,
        uint256& out_ephemeralKey,
        const uint256& joinSplitPubKey,
        uint256& out_randomSeed,
        std::array<uint256, NumInputs>& out_macs,
        std::array<uint256, NumInputs>& out_nullifiers,
        std::array<uint256, NumOutputs>& out_commitments,
        uint64_t vpub_old,
        uint64_t vpub_new,
        const uint256& rt,
        bool computeProof,
        uint256 *out_esk // Payment disclosure
    ) {
        if (vpub_old > MAX_MONEY) {
            throw std::invalid_argument("nonsensical vpub_old value");
        }

        if (vpub_new > MAX_MONEY) {
            throw std::invalid_argument("nonsensical vpub_new value");
        }

        uint64_t lhs_value = vpub_old;
        uint64_t rhs_value = vpub_new;

        for (size_t i = 0; i < NumInputs; i++) {
            // Sanity checks of input
            {
                // If note has nonzero value
                if (inputs[i].note.value() != 0) {
                    // The witness root must equal the input root.
                    if (inputs[i].witness.root() != rt) {
                        throw std::invalid_argument("joinsplit not anchored to the correct root");
                    }

                    // The tree must witness the correct element
                    if (inputs[i].note.cm() != inputs[i].witness.element()) {
                        throw std::invalid_argument("witness of wrong element for joinsplit input");
                    }
                }

                // Ensure we have the key to this note.
                if (inputs[i].note.a_pk != inputs[i].key.address().a_pk) {
                    throw std::invalid_argument("input note not authorized to spend with given key");
                }

                // Balance must be sensical
                if (inputs[i].note.value() > MAX_MONEY) {
                    throw std::invalid_argument("nonsensical input note value");
                }

                lhs_value += inputs[i].note.value();

                if (lhs_value > MAX_MONEY) {
                    throw std::invalid_argument("nonsensical left hand size of joinsplit balance");
                }
            }

            // Compute nullifier of input
            out_nullifiers[i] = inputs[i].nullifier();
        }

        // Sample randomSeed
        out_randomSeed = random_uint256();

        // Compute h_sig
        uint256 h_sig = this->h_sig(out_randomSeed, out_nullifiers, joinSplitPubKey);

        // Sample phi
        uint252 phi = random_uint252();

        // Compute notes for outputs
        for (size_t i = 0; i < NumOutputs; i++) {
            // Sanity checks of output
            {
                if (outputs[i].value > MAX_MONEY) {
                    throw std::invalid_argument("nonsensical output value");
                }

                rhs_value += outputs[i].value;

                if (rhs_value > MAX_MONEY) {
                    throw std::invalid_argument("nonsensical right hand side of joinsplit balance");
                }
            }

            // Sample r
            uint256 r = random_uint256();

            out_notes[i] = outputs[i].note(phi, r, i, h_sig);
        }

        if (lhs_value != rhs_value) {
            throw std::invalid_argument("invalid joinsplit balance");
        }

        // Compute the output commitments
        for (size_t i = 0; i < NumOutputs; i++) {
            out_commitments[i] = out_notes[i].cm();
        }

        // Encrypt the ciphertexts containing the note
        // plaintexts to the recipients of the value.
        {
            ZCNoteEncryption encryptor(h_sig);

            for (size_t i = 0; i < NumOutputs; i++) {
                SproutNotePlaintext pt(out_notes[i], outputs[i].memo);

                out_ciphertexts[i] = pt.encrypt(encryptor, outputs[i].addr.pk_enc);
            }

            out_ephemeralKey = encryptor.get_epk();

            // !!! Payment disclosure START
            if (out_esk != nullptr) {
                *out_esk = encryptor.get_esk();
            }
            // !!! Payment disclosure END
        }

        // Authenticate h_sig with each of the input
        // spending keys, producing macs which protect
        // against malleability.
        for (size_t i = 0; i < NumInputs; i++) {
            out_macs[i] = PRF_pk(inputs[i].key, i, h_sig);
        }

        if (!computeProof) {
            return GrothProof();
        }

        GrothProof proof;

        CDataStream ss1(SER_NETWORK, PROTOCOL_VERSION);
        ss1 << inputs[0].witness.path();
        std::vector<unsigned char> auth1(ss1.begin(), ss1.end());

        CDataStream ss2(SER_NETWORK, PROTOCOL_VERSION);
        ss2 << inputs[1].witness.path();
        std::vector<unsigned char> auth2(ss2.begin(), ss2.end());

        librustzcash_sprout_prove(
            proof.begin(),

            phi.begin(),
            rt.begin(),
            h_sig.begin(),

            inputs[0].key.begin(),
            inputs[0].note.value(),
            inputs[0].note.rho.begin(),
            inputs[0].note.r.begin(),
            auth1.data(),

            inputs[1].key.begin(),
            inputs[1].note.value(),
            inputs[1].note.rho.begin(),
            inputs[1].note.r.begin(),
            auth2.data(),

            out_notes[0].a_pk.begin(),
            out_notes[0].value(),
            out_notes[0].r.begin(),

            out_notes[1].a_pk.begin(),
            out_notes[1].value(),
            out_notes[1].r.begin(),

            vpub_old,
            vpub_new
        );

        return proof;
    }
};

template<size_t NumInputs, size_t NumOutputs>
JoinSplit<NumInputs, NumOutputs>* JoinSplit<NumInputs, NumOutputs>::Prepared()
{
    return new JoinSplitCircuit<NumInputs, NumOutputs>();
}

template<size_t NumInputs, size_t NumOutputs>
uint256 JoinSplit<NumInputs, NumOutputs>::h_sig(
    const uint256& randomSeed,
    const std::array<uint256, NumInputs>& nullifiers,
    const uint256& joinSplitPubKey
) {
    const unsigned char personalization[crypto_generichash_blake2b_PERSONALBYTES]
        = {'Z','c','a','s','h','C','o','m','p','u','t','e','h','S','i','g'};

    std::vector<unsigned char> block(randomSeed.begin(), randomSeed.end());

    for (size_t i = 0; i < NumInputs; i++) {
        block.insert(block.end(), nullifiers[i].begin(), nullifiers[i].end());
    }

    block.insert(block.end(), joinSplitPubKey.begin(), joinSplitPubKey.end());

    uint256 output;

    if (crypto_generichash_blake2b_salt_personal(output.begin(), 32,
                                                 &block[0], block.size(),
                                                 NULL, 0, // No key.
                                                 NULL,    // No salt.
                                                 personalization
                                                ) != 0)
    {
        throw std::logic_error("hash function failure");
    }

    return output;
}

SproutNote JSOutput::note(const uint252& phi, const uint256& r, size_t i, const uint256& h_sig) const {
    uint256 rho = PRF_rho(phi, i, h_sig);

    return SproutNote(addr.a_pk, value, rho, r);
}

JSOutput::JSOutput() : addr(uint256(), uint256()), value(0) {
    SproutSpendingKey a_sk = SproutSpendingKey::random();
    addr = a_sk.address();
}

JSInput::JSInput() : witness(SproutMerkleTree().witness()),
                     key(SproutSpendingKey::random()) {
    note = SproutNote(key.address().a_pk, 0, random_uint256(), random_uint256());
    SproutMerkleTree dummy_tree;
    dummy_tree.append(note.cm());
    witness = dummy_tree.witness();
}

template class JoinSplit<ZC_NUM_JS_INPUTS,
                         ZC_NUM_JS_OUTPUTS>;

}


//
// 检测器方法实现
// Created by willow li on 2019/3/26.
//

#include <dlib/image_processing/frontal_face_detector.h>
#include <include/opencv2/core/mat.hpp>
#include <include/opencv2/imgproc/imgproc_c.h>
#include <include/opencv2/imgproc/types_c.h>
#include <src/main/cpp/dlib/opencv/cv_image.h>
#include <include/opencv2/imgproc.hpp>
#include "config.h"
#include "face_detector.h"

using namespace out_cast_detector;
using namespace cv;
using namespace dlib;

face_detector::face_detector() = default;

void face_detector::init_face_detector(JNIEnv *env, jstring predictor_path) {
    LOGI("init_face_detector start");
    string path = jstring_complier::jstring_to_string(env, predictor_path);
    detector = dlib::get_frontal_face_detector();
    predictor = shape_predictor();
    deserialize(path) >> predictor;
    // deserialize(predictor, path);

    jclass pointFClazz = (env)->FindClass("android/graphics/PointF");
    jclass faceInfoClass = (env)->FindClass("com/video/facedetector/FaceInfo");
    mPointClass = (jclass) (env)->NewGlobalRef(pointFClazz);
    mFaceInfoClass = (jclass) (env)->NewGlobalRef(faceInfoClass);
    pointConstructID = (env)->GetMethodID(mPointClass, "<init>", "()V");
    faceConstructID = (env)->GetMethodID(mFaceInfoClass, "<init>", "()V");
    LOGI("init_face_detector finish");
}

jobjectArray face_detector::do_face_detect_action(JNIEnv *env,
                                                  jbyteArray image_data,
                                                  jint image_height,
                                                  jint image_widht) {
    try {
        if (image_data == nullptr || image_height <= 0 || image_widht <= 0) {
            return nullptr;
        }
        // 灰度取值
        LOGI("数据转换 start");
        array2d<unsigned char> frame =
                jstring_complier::jbyteArray_to_array2dGrayscale(
                        env,
                        image_data,
                        image_height,
                        image_widht
                );
        LOGI("数据转换 finish");

        // 人脸检测
        LOGI("人脸检测 start");
        detector = dlib::get_frontal_face_detector();
        std::vector<dlib::rectangle> rects = detector(frame);
        LOGI("至少有人脸? %i", (int) jsize(rects.size()));
        jobjectArray final_result =
                (env)->NewObjectArray(
                        jsize(rects.size()),
                        mFaceInfoClass,
                        nullptr
                );
        for (int i = 0; i < rects.size(); ++i) {
            full_object_detection faces_landmark = predictor(frame, rects[i]);
            jobjectArray keymarks = (env)->NewObjectArray(68, mPointClass, nullptr);
            for (int index = 0; index < 68; index++) {
                point p = faces_landmark.part((unsigned long) (index));
                jobject tempPoint = (env)->NewObject(mPointClass, pointConstructID);
                jfieldID px = (env)->GetFieldID(mPointClass, "x", "F");
                jfieldID py = (env)->GetFieldID(mPointClass, "y", "F");
                (env)->SetFloatField(tempPoint, px, (float) p.x());
                (env)->SetFloatField(tempPoint, py, (float) p.y());
                (env)->SetObjectArrayElement(keymarks, index, tempPoint);
                (env)->DeleteLocalRef(tempPoint);
            }
            jobject tempFaceInfo = (env)->NewObject(mFaceInfoClass, faceConstructID);
            jfieldID marksId = (env)->GetFieldID(mFaceInfoClass, "mKeyPoints",
                                                 "[Landroid/graphics/PointF;");
            (env)->SetObjectField(tempFaceInfo, marksId, keymarks);
            (env)->SetObjectArrayElement(final_result, i, tempFaceInfo);
            LOGI("单个人脸数据释放前");
            (env)->DeleteLocalRef(tempFaceInfo);
            (env)->DeleteLocalRef(keymarks);
        }
        LOGI("人脸检测 finish");
        rects.clear();
        frame.clear();
        return final_result;

    } catch (exception &e) {
        LOGE("%s", e.what());
    }
    return nullptr;
}

jobjectArray face_detector::do_face_detect_action_mat(JNIEnv *env,
                                                      const cv::Mat &image_data,
                                                      jint image_height,
                                                      jint image_widht) {
    /*try {
        if (image_data.empty() || image_height <= 0 || image_widht <= 0) {
            return nullptr;
        }
        // 灰度取值
        LOGI("数据转换 start");
        if (image_data.channels() == 1) {
            cv::cvtColor(image_data, image_data, CV_GRAY2BGR);
        }

        dlib::cv_image<dlib::bgr_pixel> frame(image_data);
        LOGI("数据转换 finish");

        // 人脸检测
        LOGI("人脸检测 start");
        std::vector<dlib::rectangle> rects = detector(frame);
        LOGI("至少有人脸? %i", (int) jsize(rects.size()));
        jobjectArray final_result =
                (env)->NewObjectArray(
                        jsize(rects.size()),
                        mFaceInfoClass,
                        nullptr
                );
        for (int i = 0; i < rects.size(); ++i) {
            full_object_detection faces_landmark = predictor(frame, rects[i]);
            jobjectArray keymarks = (env)->NewObjectArray(68, mPointClass, nullptr);
            for (int index = 0; index < 68; index++) {
                point p = faces_landmark.part((unsigned long) (index));
                jobject tempPoint = (env)->NewObject(mPointClass, pointConstructID);
                jfieldID px = (env)->GetFieldID(mPointClass, "x", "F");
                jfieldID py = (env)->GetFieldID(mPointClass, "y", "F");
                (env)->SetFloatField(tempPoint, px, (float) p.x());
                (env)->SetFloatField(tempPoint, py, (float) p.y());
                (env)->SetObjectArrayElement(keymarks, index, tempPoint);
                (env)->DeleteLocalRef(tempPoint);
            }
            jobject tempFaceInfo = (env)->NewObject(mFaceInfoClass, faceConstructID);
            jfieldID marksId = (env)->GetFieldID(mFaceInfoClass, "mKeyPoints",
                                                 "[Landroid/graphics/PointF;");
            (env)->SetObjectField(tempFaceInfo, marksId, keymarks);
            (env)->SetObjectArrayElement(final_result, i, tempFaceInfo);
            LOGI("单个人脸数据释放前");
            (env)->DeleteLocalRef(tempFaceInfo);
            (env)->DeleteLocalRef(keymarks);
        }
        LOGI("人脸检测 finish");
        rects.clear();
        return final_result;

    } catch (exception &e) {
        LOGE("%s", e.what());
    }*/
    return nullptr;
}

/*暴露方法=======================================================================================*/
face_detector *current_detector = nullptr;

static void out_cast_detector::do_init(JNIEnv *env,
                                       jobject job,
                                       jstring predictor_path) {
    LOGI("init_face_detector proxy start");
    if (current_detector != nullptr) {
        current_detector->init_face_detector(env, predictor_path);
    }
    LOGI("init_face_detector proxy finish");
}

static jobjectArray out_cast_detector::do_detect(JNIEnv *env,
                                                 jobject obj,
                                                 jbyteArray image_data,
                                                 jint image_height,
                                                 jint image_widht) {
    LOGI("do_detect proxy");
    if (current_detector != nullptr) {
        return current_detector->do_face_detect_action(env, image_data, image_height, image_widht);
    }
    return nullptr;
}

static jobjectArray out_cast_detector::do_detect_mat(JNIEnv *env,
                                                     const cv::Mat &image_data,
                                                     jint image_height,
                                                     jint image_widht) {
    LOGI("do_detect proxy");
    if (current_detector != nullptr) {
        return current_detector->do_face_detect_action_mat(env, image_data, image_height,
                                                           image_widht);
    }
    return nullptr;
}

/*动态注册=======================================================================================*/

// 方法声明
static const char *jniClassName = "com/video/facedetector/FaceDetectorManager";
static const JNINativeMethod provide_methods[] = {
        {"initFaceDetector",      "(Ljava/lang/String;)V",                      (void *) out_cast_detector::do_init},
        {"doFaceDetectAction",    "([BII)[Lcom/video/facedetector/FaceInfo;", (jobjectArray *) out_cast_detector::do_detect},
        {"doFaceDetectMatAction", "([BII)[Lcom/video/facedetector/FaceInfo;", (jobjectArray *) out_cast_detector::do_detect_mat},
};

// 此函数通过调用RegisterNatives方法来注册我们的函数
static int registerNativeMethods(JNIEnv *env,
                                 const char *className,
                                 const JNINativeMethod *getMethods,
                                 int methodsNum) {
    jclass clazz;
    clazz = (env)->FindClass(className);
    if (clazz == nullptr) {
        return JNI_FALSE;
    }
    if ((env)->RegisterNatives(clazz, getMethods, methodsNum) < 0) {
        return JNI_FALSE;
    }
    if (current_detector == nullptr) {
        current_detector = new face_detector();
    }
    return JNI_TRUE;
}

// 通用动态注册函数
static int register_android_face_detector(JNIEnv *env) {
    return registerNativeMethods(env,
                                 jniClassName,
                                 provide_methods,
                                 sizeof(provide_methods) / sizeof(provide_methods[0]));
}

/*动态注册：生命周期===================================================================================*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    assert(env != nullptr);
    if (!register_android_face_detector(env)) {
        return -1;
    }
    return JNI_VERSION_1_6;
}

JNIEXPORT void JNI_OnUnload(JavaVM *vm, void *reserved) {

}


// Boost.Geometry (aka GGL, Generic Geometry Library)

// Copyright (c) 2007-2014 Barend Gehrels, Amsterdam, the Netherlands.
// Copyright (c) 2008-2014 Bruno Lalande, Paris, France.
// Copyright (c) 2009-2014 Mateusz Loskot, London, UK.

// This file was modified by Oracle on 2014, 2015.
// Modifications copyright (c) 2014-2015, Oracle and/or its affiliates.

// Contributed and/or modified by Menelaos Karavelas, on behalf of Oracle
// Contributed and/or modified by Adam Wulkiewicz, on behalf of Oracle

// Parts of Boost.Geometry are redesigned from Geodan's Geographic Library
// (geolib/GGL), copyright (c) 1995-2010 Geodan, Amsterdam, the Netherlands.

// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#ifndef BOOST_GEOMETRY_ALGORITHMS_LENGTH_HPP
#define BOOST_GEOMETRY_ALGORITHMS_LENGTH_HPP

#include <iterator>

#include <boost/concept_check.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/range.hpp>

#include <boost/mpl/fold.hpp>
#include <boost/mpl/greater.hpp>
#include <boost/mpl/if.hpp>
#include <boost/mpl/insert.hpp>
#include <boost/mpl/int.hpp>
#include <boost/mpl/set.hpp>
#include <boost/mpl/size.hpp>
#include <boost/mpl/transform.hpp>
#include <boost/type_traits.hpp>

#include <boost/variant/apply_visitor.hpp>
#include <boost/variant/static_visitor.hpp>
#include <boost/variant/variant_fwd.hpp>

#include <boost/geometry/core/cs.hpp>
#include <boost/geometry/core/closure.hpp>
#include <boost/geometry/core/tags.hpp>

#include <boost/geometry/geometries/concepts/check.hpp>

#include <boost/geometry/algorithms/assign.hpp>
#include <boost/geometry/algorithms/detail/calculate_null.hpp>
#include <boost/geometry/algorithms/detail/multi_sum.hpp>
// #include <boost/geometry/algorithms/detail/throw_on_empty_input.hpp>
#include <boost/geometry/views/closeable_view.hpp>
#include <boost/geometry/strategies/distance.hpp>
#include <boost/geometry/strategies/default_length_result.hpp>

namespace geofeatures_boost {} namespace boost = geofeatures_boost; namespace geofeatures_boost { namespace geometry
{

#ifndef DOXYGEN_NO_DETAIL
namespace detail { namespace length
{

template<typename Segment>
struct segment_length
{
    template <typename Strategy>
    static inline typename default_length_result<Segment>::type apply(
            Segment const& segment, Strategy const& strategy)
    {
        geofeatures_boost::ignore_unused(strategy);
        typedef typename point_type<Segment>::type point_type;
        point_type p1, p2;
        geometry::detail::assign_point_from_index<0>(segment, p1);
        geometry::detail::assign_point_from_index<1>(segment, p2);
        return strategy.apply(p1, p2);
    }
};

/*!
\brief Internal, calculates length of a linestring using iterator pairs and
    specified strategy
\note for_each could be used here, now that point_type is changed by boost
    range iterator
*/
template<typename Range, closure_selector Closure>
struct range_length
{
    typedef typename default_length_result<Range>::type return_type;

    template <typename Strategy>
    static inline return_type apply(
            Range const& range, Strategy const& strategy)
    {
        geofeatures_boost::ignore_unused(strategy);
        typedef typename closeable_view<Range const, Closure>::type view_type;
        typedef typename geofeatures_boost::range_iterator
            <
                view_type const
            >::type iterator_type;

        return_type sum = return_type();
        view_type view(range);
        iterator_type it = geofeatures_boost::begin(view), end = geofeatures_boost::end(view);
        if(it != end)
        {
            for(iterator_type previous = it++;
                    it != end;
                    ++previous, ++it)
            {
                // Add point-point distance using the return type belonging
                // to strategy
                sum += strategy.apply(*previous, *it);
            }
        }

        return sum;
    }
};

}} // namespace detail::length
#endif // DOXYGEN_NO_DETAIL

#ifndef DOXYGEN_NO_DISPATCH
namespace dispatch
{

template <typename Geometry, typename Tag = typename tag<Geometry>::type>
struct length : detail::calculate_null
{
    typedef typename default_length_result<Geometry>::type return_type;

    template <typename Strategy>
    static inline return_type apply(Geometry const& geometry, Strategy const& strategy)
    {
        return calculate_null::apply<return_type>(geometry, strategy);
    }
};

template <typename Geometry>
struct length<Geometry, linestring_tag>
    : detail::length::range_length<Geometry, closed>
{};

// RING: length is currently 0; it might be argued that it is the "perimeter"

template <typename Geometry>
struct length<Geometry, segment_tag>
    : detail::length::segment_length<Geometry>
{};

template <typename MultiLinestring>
struct length<MultiLinestring, multi_linestring_tag> : detail::multi_sum
{
    template <typename Strategy>
    static inline typename default_length_result<MultiLinestring>::type
    apply(MultiLinestring const& multi, Strategy const& strategy)
    {
        return multi_sum::apply
               <
                   typename default_length_result<MultiLinestring>::type,
                   detail::length::range_length
                   <
                       typename geofeatures_boost::range_value<MultiLinestring>::type,
                       closed // no need to close it explicitly
                   >
               >(multi, strategy);

    }
};

} // namespace dispatch
#endif // DOXYGEN_NO_DISPATCH

namespace resolve_variant {

template <typename Geometry>
struct length
{
    template <typename Strategy>
    static inline typename default_length_result<Geometry>::type
    apply(Geometry const& geometry, Strategy const& strategy)
    {
        return dispatch::length<Geometry>::apply(geometry, strategy);
    }
};

template <BOOST_VARIANT_ENUM_PARAMS(typename T)>
struct length<geofeatures_boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> >
{
    typedef typename default_length_result
        <
            geofeatures_boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)>
        >::type result_type;

    template <typename Strategy>
    struct visitor
        : static_visitor<result_type>
    {
        Strategy const& m_strategy;

        visitor(Strategy const& strategy)
            : m_strategy(strategy)
        {}

        template <typename Geometry>
        inline typename default_length_result<Geometry>::type
        operator()(Geometry const& geometry) const
        {
            return length<Geometry>::apply(geometry, m_strategy);
        }
    };

    template <typename Strategy>
    static inline result_type apply(
        variant<BOOST_VARIANT_ENUM_PARAMS(T)> const& geometry,
        Strategy const& strategy
    )
    {
        return geofeatures_boost::apply_visitor(visitor<Strategy>(strategy), geometry);
    }
};

} // namespace resolve_variant

/*!
\brief \brief_calc{length}
\ingroup length
\details \details_calc{length, length (the sum of distances between consecutive points)}. \details_default_strategy
\tparam Geometry \tparam_geometry
\param geometry \param_geometry
\return \return_calc{length}

\qbk{[include reference/algorithms/length.qbk]}
\qbk{[length] [length_output]}
 */
template<typename Geometry>
inline typename default_length_result<Geometry>::type
length(Geometry const& geometry)
{
    concept::check<Geometry const>();

    // detail::throw_on_empty_input(geometry);

    // TODO put this into a resolve_strategy stage
    typedef typename strategy::distance::services::default_strategy
        <
            point_tag, point_tag, typename point_type<Geometry>::type
        >::type strategy_type;

    return resolve_variant::length<Geometry>::apply(geometry, strategy_type());
}

/*!
\brief \brief_calc{length} \brief_strategy
\ingroup length
\details \details_calc{length, length (the sum of distances between consecutive points)} \brief_strategy. \details_strategy_reasons
\tparam Geometry \tparam_geometry
\tparam Strategy \tparam_strategy{distance}
\param geometry \param_geometry
\param strategy \param_strategy{distance}
\return \return_calc{length}

\qbk{distinguish,with strategy}
\qbk{[include reference/algorithms/length.qbk]}
\qbk{[length_with_strategy] [length_with_strategy_output]}
 */
template<typename Geometry, typename Strategy>
inline typename default_length_result<Geometry>::type
length(Geometry const& geometry, Strategy const& strategy)
{
    concept::check<Geometry const>();

    // detail::throw_on_empty_input(geometry);

    return resolve_variant::length<Geometry>::apply(geometry, strategy);
}

}} // namespace geofeatures_boost::geometry

#endif // BOOST_GEOMETRY_ALGORITHMS_LENGTH_HPP


#include <algorithm>
#include <fstream>
#include <numbers>

#include <boost/endian.hpp>

#include <range/v3/view.hpp>

#include <spdlog/spdlog.h>

#include <zlib.h>

#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>

#include <osmpbf/fileformat.pb.h>
#include <osmpbf/osmformat.pb.h>

std::optional<std::uint32_t> value_for_key(auto &&element,
                                           const std::uint32_t &key) {
  for (decltype(element.keys().size()) i = 0; i < element.keys().size(); ++i) {
    if (element.keys(i) == key) {
      return element.vals(i);
    }
  }

  return std::nullopt;
}

bool has_key(auto &&element, const std::uint32_t &key) {
  return std::ranges::find(element.keys(), key) != element.keys().end();
}

bool has_attribute(auto &&element, const std::uint32_t &key,
                   const std::uint32_t &value) {
  const auto entries = ranges::views::zip(element.keys(), element.vals());
  return std::ranges::find(entries, std::make_pair(key, value)) !=
         entries.end();
}

bool has_attribute(auto &&keys, auto &&vals, const std::uint32_t &key,
                   const std::uint32_t &value) {
  const auto entries = ranges::views::zip(keys, vals);
  return std::ranges::find(entries, std::make_pair(key, value)) !=
         entries.end();
}

struct StringAttributeMap {
  StringAttributeMap(const OSMPBF::PrimitiveBlock &block) {
    for (int i = 0; i < block.stringtable().s_size(); ++i) {
      const auto &s = block.stringtable().s(i);
      if (s == "highway") {
        highway = i;
      } else if (s == "street_lamp") {
        street_lamp = i;
      } else if (s == "lit") {
        lit = i;
      } else if (s == "no") {
        no = i;
      } else if (s == "tower:type") {
        tower_type = i;
      } else if (s == "lighting") {
        lighting = i;
      }
    }
  }

  std::uint32_t highway = 0;
  std::uint32_t street_lamp = 0;
  std::uint32_t lit = 0;
  std::uint32_t no = 0;
  std::uint32_t tower_type = 0;
  std::uint32_t lighting = 0;
};

struct BoundingBox {
  static BoundingBox fromPoints(auto &&points) {
    BoundingBox result;

    for (const auto &point : points) {
      result.min.x = std::min(result.min.x, point.x);
      result.min.y = std::min(result.min.y, point.y);
      result.max.x = std::max(result.max.x, point.x);
      result.max.y = std::max(result.max.y, point.y);
    }

    return result;
  }

  auto ratio() const { return (max.x - min.x) / (max.y - min.y); }

  using numeric_limits = std::numeric_limits<cv::Point2d::value_type>;
  cv::Point2d min{numeric_limits::infinity(), numeric_limits::infinity()};
  cv::Point2d max{-numeric_limits::infinity(), -numeric_limits::infinity()};
};

struct Node {
  Node(double lon, double lat, bool is_street_lamp)
      : lon{lon}, lat{lat}, is_street_lamp{is_street_lamp} {
    using std::numbers::pi;
    using namespace std;
    const auto rad = [](const auto &deg) { return deg / 180.0 * pi; };

    x = 1.0 / (2 * pi) * 2 * (rad(lon) + pi);
    y = 1.0 / (2 * pi) * 2 * (pi - log(tan(pi / 4.0 + rad(lat) / 2.0)));
  }

  double lon;
  double lat;
  bool is_street_lamp;

  double x;
  double y;

  size_t refcount = 0;
};

using NodeRef = std::int64_t;

struct Road {
  std::vector<NodeRef> nodes;
  std::optional<bool> is_lit;
};

std::vector<char> zlib(const OSMPBF::Blob &blob) {
  decltype(zlib(blob)) result;

  if (!blob.has_zlib_data()) {
    return result;
  }

  result.resize(blob.raw_size());
  size_t uncompressedSize = result.size();
  if (uncompress(reinterpret_cast<Bytef *>(result.data()), &uncompressedSize,
                 reinterpret_cast<Bytef *>(
                     const_cast<char *>(blob.zlib_data().c_str())),
                 blob.zlib_data().size()) != Z_OK) {
    spdlog::critical("[zlib] uncompress failed.");
  }

  if (uncompressedSize != result.size()) {
    spdlog::critical(
        "[zlib] uncompressed size does not match expected one in header.");
  }

  return result;
}

size_t read_pbf_segment(std::ifstream &stream,
                        std::unordered_map<NodeRef, Node> &points,
                        std::vector<Road> &roads) {
  boost::endian::big_int32_t size;
  stream.read(reinterpret_cast<char *>(&size), sizeof(size));

  if (size == 0) {
    return 0;
  }

  std::vector<char> buffer(size);
  stream.read(buffer.data(), buffer.size());

  OSMPBF::BlobHeader blobheader;
  if (!blobheader.ParseFromArray(buffer.data(), buffer.size())) {
    spdlog::critical(
        "Failed to parse OSMPBF::BlobHeader (size is given as {} bytes).",
        size);
    return 0;
  }

  buffer.resize(blobheader.datasize());
  stream.read(buffer.data(), buffer.size());
  OSMPBF::Blob blob;
  if (!blob.ParseFromArray(buffer.data(), buffer.size())) {
    spdlog::critical(
        "Failed to parse OSMPBF::Blob (size is given as {} bytes).",
        blobheader.datasize());
    return 0;
  }

  if (!blob.has_zlib_data() || blob.has_lzma_data() || blob.has_raw()) {
    spdlog::critical(
        "[read_pbf_segment] Found a segment that is not zlib compressed. Only "
        "zlib compressed segments are supported.");
    return 0;
  }

  auto blockbuffer = zlib(blob);

  if (blobheader.type() == "OSMHeader") {

  } else if (blobheader.type() == "OSMData") {
    OSMPBF::PrimitiveBlock block;
    if (!block.ParseFromArray(blockbuffer.data(), blockbuffer.size())) {
      spdlog::critical("Failed to parse block");
    }

    const auto unpackLongitude = [&](const auto &packedLon) {
      return 0.000000001 *
             (block.lon_offset() + (block.granularity() * packedLon));
    };

    const auto unpackLatitude = [&](const auto &packedLat) {
      return 0.000000001 *
             (block.lat_offset() + (block.granularity() * packedLat));
    };

    const StringAttributeMap strings{block};

    for (const auto &group : block.primitivegroup()) {
      const auto is_street_lamp = [&](const auto &...args) {
        return has_attribute(args..., strings.highway, strings.street_lamp) ||
               has_attribute(args..., strings.tower_type, strings.lighting);
      };

      //
      // Nodes
      //
      std::transform(
          group.nodes().begin(), group.nodes().end(),
          std::inserter(points, points.begin()),
          [&](const auto &node) -> std::decay_t<decltype(points)>::value_type {
            return {std::piecewise_construct, std::forward_as_tuple(node.id()),
                    std::forward_as_tuple(unpackLongitude(node.lon()),
                                          unpackLatitude(node.lat()),
                                          is_street_lamp(node))};
          });

      //
      // DenseNodes
      //
      if (group.has_dense()) {
        const auto &dense = group.dense();

        std::int64_t id = 0;
        std::int64_t packedLat = 0;
        std::int64_t packedLon = 0;

        using namespace ranges::views;
        for (const auto &[delta_id, delta_lat, delta_lon, attr] :
             zip(dense.id(), dense.lat(), dense.lon(),
                 dense.keys_vals() | split(0))) {
          id += delta_id;
          packedLat += delta_lat;
          packedLon += delta_lon;

          const auto keys = attr | stride(2);
          const auto vals = attr | tail | stride(2);
          points.emplace(std::piecewise_construct, std::forward_as_tuple(id),
                         std::forward_as_tuple(unpackLongitude(packedLon),
                                               unpackLatitude(packedLat),
                                               is_street_lamp(keys, vals)));
        }
      }

      //
      // Ways
      //
      const auto is_highway = [&](const auto &way) {
        return has_key(way, strings.highway);
      };

      std::ranges::transform(group.ways() | std::views::filter(is_highway),
                             std::back_inserter(roads), [&](const auto &road) {
                               std::decay_t<decltype(roads)>::value_type result;

                               if (const auto lit_attr =
                                       value_for_key(road, strings.lit)) {
                                 result.is_lit = *lit_attr != strings.no;
                               }

                               std::int64_t node = 0;
                               result.nodes.reserve(road.refs().size());
                               for (const auto &delta_ref : road.refs()) {
                                 node += delta_ref;
                                 result.nodes.emplace_back(node);
                                 ++points.at(node).refcount;
                               }

                               return result;
                             });
    }
  }

  return blob.raw_size();
}

int main(int argc, char *argv[]) {
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  if (argc != 3) {
    spdlog::critical("Usage: {} pbf-file out-image-file", argv[0]);
    return -1;
  }

  const auto inputFile = argv[1];
  const auto outputFile = argv[2];

  //
  // Parse OSM export
  //

  std::ifstream file{inputFile, std::ios::binary};
  if (!file.is_open()) {
    spdlog::critical("Failed to open PBF file \"{}\".", inputFile);
    return -2;
  }

  std::unordered_map<NodeRef, Node> nodes;
  std::vector<Road> roads;

  spdlog::info("Start parsing of PBF file.");

  size_t raw_size = 0;
  while (!file.eof()) {
    raw_size += read_pbf_segment(file, nodes, roads);
  }

  spdlog::info("Parsed {} bytes of data containing {} nodes and {} ways (with "
               "highway attribute).",
               raw_size, nodes.size(), roads.size());

  //
  // Define canvas
  //
  const auto bb = BoundingBox::fromPoints(
      nodes | std::views::values | std::views::filter([](const auto &point) {
        return point.refcount > 0 || point.is_street_lamp;
      }));
  cv::Mat image = cv::Mat::zeros(1024 * 5 / bb.ratio(), 1024 * 5, CV_8UC3);

  const auto imagePostion = [&](const auto &point) -> cv::Point2d {
    const auto x = [&](const auto &p) {
      return (p.x - bb.min.x) / (bb.max.x - bb.min.x) * image.cols;
    };

    const auto y = [&](const auto &p) {
      return (p.y - bb.min.y) / (bb.max.y - bb.min.y) * image.rows;
    };

    return {x(point), y(point)};
  };

  //
  // Draw Roads
  //
  size_t numLitRoads = 0;
  size_t numNonLitRoads = 0;
  size_t numRoadsWithoutLitTag = 0;

  for (const auto &road : roads) {
    const auto color = [&]() -> cv::Scalar {
      if (road.is_lit.has_value()) {
        if (road.is_lit.value()) {
          ++numLitRoads;
          return {0, 190, 190};
        } else {
          ++numNonLitRoads;
          return {0, 0, 63};
        }
      } else {
        ++numRoadsWithoutLitTag;
        return {63, 0, 0};
      }
    }();

    for (const auto &[startIndex, endIndex] :
         ranges::views::zip(road.nodes | ranges::views::drop_last(1),
                            road.nodes | ranges::views::drop(1))) {
      const auto &start = nodes.at(startIndex);
      const auto &end = nodes.at(endIndex);
      cv::line(image, imagePostion(start), imagePostion(end), color);
    }
  }
  spdlog::info("Lit: Yes={}, No={}, Unknown={}.", numLitRoads, numNonLitRoads,
               numRoadsWithoutLitTag);

  //
  // Draw Street Lamps
  //
  size_t numStreetLamps = 0;
  for (const auto &lamp :
       nodes | std::views::values | std::views::filter([](const auto &point) {
         return point.is_street_lamp;
       })) {
    cv::circle(image, imagePostion(lamp), 2, {0, 255, 255}, cv::FILLED);
    ++numStreetLamps;
  }
  spdlog::info("Found {} street lamps.", numStreetLamps);

  if (!cv::imwrite(outputFile, image)) {
    spdlog::critical("Failed to write output to \"{}\".", outputFile);
    return -3;
  }

  return 0;
}