# frozen_string_literal: true
require 'glitch3d/version'
require 'glitch3d/objects/vertex'
require 'glitch3d/objects/face'

Dir[File.dirname(__FILE__) + '/glitch3d/strategies/*.rb'].each { |file| require file }

module Glitch3d
  VERTEX_GLITCH_ITERATION_RATIO = 0.1
  VERTEX_GLITCH_OFFSET = 1

  FACE_GLITCH_ITERATION_RATIO = 0.1
  FACE_GLITCH_OFFSET = 0.5
  BOUNDARY_LIMIT = 4 # Contain model within 2x2x2 cube
  CHUNK_SIZE=20

  BLENDER_EXECUTABLE_PATH = ENV['BLENDER_EXECUTABLE_PATH'].freeze
  RENDERING_SCRIPT_PATH = File.dirname(__FILE__) + '/glitch3d/bpy/main.py'

  def clean_model(source_file)
    self.class.include Glitch3d::None
    base_file_name = source_file.gsub(/.obj/, '')
    model_name = File.basename(source_file, '.obj')
    target_file = base_file_name + '.obj'
    create_glitched_file(glitch(read_source(source_file)), target_file, model_name)
  end

  # @param String source_file, 3d model file to take as input
  # @param Hash args, parameters { 'stuff' => 'shit' }
  def process_model(source_file, args = nil)
    raise 'Make sure Blender is correctly installed' if BLENDER_EXECUTABLE_PATH.nil?
    source_file = random_fixture if source_file.nil?
    args = Hash[ARGV.join(' ').scan(/--?([^=\s]+)(?:=(\S+))?/)] if args.nil?
    return clean_model(source_file) if args['clean']

    if args.has_key?('version')
      puts Glitch3d::VERSION
      return nil
    end

    if args.has_key?('animate')
      args['animate'] = 'true'
    else
      args['animate'] = 'false'
    end

    raise 'Set Blender executable path in your env variables before using glitch3d' if BLENDER_EXECUTABLE_PATH.nil?
    self.class.include infer_strategy(args['mode'] || 'default')
    @quality = args['quality'] || 'low'
    source_file = source_file
    base_file_name = source_file&.gsub(/.obj/, '')
    model_name = File.basename(source_file, '.obj')
    target_file = base_file_name + '_glitched.obj'
    create_glitched_file(glitch(read_source(source_file)), target_file, model_name)
    render(args, target_file, args['shots-number'] || 6) unless args['no-render']
  end

  def random_fixture
    @fixtures_path = File.dirname(__FILE__) + '/../fixtures'
    fixtures = []
    Dir.foreach(@fixtures_path) do |item|
      next if item == '.' or item == '..' or item.end_with?('_glitched.obj') or !item.end_with?('.obj')
      fixtures << @fixtures_path + '/' + item
    end
    fixtures.sample
  end

  def infer_strategy(mode)
    return [ Glitch3d::Default, Glitch3d::Duplication, Glitch3d::FindAndReplace, Glitch3d::Localized, Glitch3d::None].sample if mode.nil?
    begin
      return eval("Glitch3d::#{mode.to_s.gsub(/(?:_|^)(\w)/){$1.upcase}}")
    rescue
      raise "Strategy #{mode.to_s..gsub(/(?:_|^)(\w)/){$1.upcase}} not found"
    end
  end

  class << self
    def rand_vertex_glitch_offset
      rand(-VERTEX_GLITCH_OFFSET..VERTEX_GLITCH_OFFSET)
    end
  end

  private

  def read_source(path)
    File.open(path, 'r') do |f|
      source_file_content = f.readlines
      vertices_list = build_vertices(source_file_content.select { |s| s[0..1] == 'v ' })
      faces_list = build_faces(source_file_content.select { |s| s[0..1] == 'f ' }, vertices_list)
      {
        vertices: vertices_list,
        faces: faces_list
      }
    end
  end

  def build_vertices(vertices_string_array)
    vertices_list = []
    vertices_string_array.each_with_index.map do |sv, i|
      v = sv.split(' ')
      vertices_list << Vertex.new(v[1].to_f, v[2].to_f, v[3].to_f, i)
    end
    vertices_list
  end

  def build_faces(faces_string_array, vertices_list)
    faces_list = []
    faces_string_array.map do |sf|
      f = sf.split(' ')
      next if f.length <= 3
      faces_list << Face.new(
        vertices_list[f[1].to_i],
        vertices_list[f[2].to_i],
        vertices_list[f[3].to_i]
      )
    end
    faces_list
  end

  def glitch(file_hash_content)
    {
      vertices: alter_vertices(file_hash_content[:vertices]),
      faces: alter_faces(file_hash_content[:faces], file_hash_content[:vertices])
    }
  end

  def random_element(array)
    array[rand(0..array.size - 1)]
  end

  def create_glitched_file(content_hash, target_file, model_name)
    boundaries = Vertex.boundaries(content_hash[:vertices])
    puts boundaries.to_s
    while rescale_needed?(boundaries)
      content_hash[:vertices] = Vertex.rescale(content_hash[:vertices], (boundaries.flatten.map(&:abs).max.abs - BOUNDARY_LIMIT).abs)
      boundaries = Vertex.boundaries(content_hash[:vertices])
    end
    boundaries = Vertex.boundaries(content_hash[:vertices])
    File.open(target_file, 'w') do |f|
      f.puts '# Data corrupted with glitch3D script'
      f.puts model_name
      f.puts '# Boundaries: ' + boundaries.to_s
      f.puts ''
      f.puts "g glitch3d"
      f.puts ''
      f.puts content_hash[:vertices].map(&:to_s)
      f.puts ''
      f.puts content_hash[:faces].map(&:to_s).compact
    end
  end

  def rescale_needed?(boundaries)
    boundaries.flatten.map(&:abs).max.abs > BOUNDARY_LIMIT
  end

  def render(initial_args, file_path, shots_number)
    raise 'Animation arg not boolean' unless eval(initial_args['animate']).is_a?(FalseClass) || eval(initial_args['animate']).is_a?(TrueClass)
    args = [
      BLENDER_EXECUTABLE_PATH,
      '-b',
      '-P',
      RENDERING_SCRIPT_PATH,
      '--',
      '-f',
      file_path,
      '-n',
      shots_number.to_s,
      '-m',
      @quality,
      '-p',
      File.dirname(__FILE__).to_s,
      '-a',
      initial_args['animate'].capitalize
    ]
    system(*args)
  end
end