package Lego::Ldraw::Display;

use strict;
use warnings; no warnings qw/void uninitialized/;

use Carp;

use Math::Trig;
use Math::VectorReal;

use OpenGL qw/ :all /;

use Lego::Ldraw::Line;
use Lego::Ldraw;

my $self = {};

##########################################################
# stuff for playing around
##########################################################

##########################################################
# end of stuff for playing around
##########################################################

sub new {
  my $proto = shift;
  my $class = ref $proto || $proto;

  $self->{width}      = shift || 300;
  $self->{height}     = shift || 300;
  $self->{name}       = 'LDraw OpenGL Display';

  my $ldraw = shift;
  $self->{ldraw} = \$ldraw
    if $ldraw;

  $self->{changed}    = undef;    # whether model has been changed

  $self->{camera} = [400, 0, -270];
  $self->{lookat} = [0, 0, 0];

  $self->{cameramode} = undef;    # how camera is moved around
  $self->{gl_init}  = {};         # gl variables;
  $self->{bindings} = {};         # bindings

  $self->{light_ambient}  = [ 0.5, 0.5, 0.5, 1.0 ];
  $self->{light_diffuse}  = [ 1, 1, 1, 1.0 ];
  $self->{light_position} = [ 2000.0, 2000.0, 1000.0, 1.0 ];
  $self->{clearcolor}     = [0.9, 0.9, 1, 0.0];

  $self->{specialkeypressed} = {};
  $self->{keypressed}        = {};

  $self->{nostuds} = 1;

  bless ($self, $class);
  return $self;
}

######################################################
# start of field access functions
######################################################

sub camera {
  my $self = shift;
  if (@_) { $self->{camera} = [@_] }
  return @{ $self->{camera} };
}


use Math::Trig ':radial';
sub move_camera {
  my $self = shift;
  my ($how, $what, $qty) = @_;
  for ($how) {
    /^x/ && do {
      $qty = $qty || 8;
      for ($what) {
	(/x/ || /1/) && do {
	  $self->{camera}->[0] += $qty;
	};
	(/y/ || /2/) && do {
	  $self->{camera}->[1] += $qty;
	};
	(/z/ || /3/) && do {
	  $self->{camera}->[2] += $qty;
	};
      };
      last;
    };
    /^s/ && do {
      my ($x, $y, $z) = @{ $self->{camera} };
      $qty = $qty || 12;
      my ($rho, $theta, $phi) = cartesian_to_spherical ($x, $z, $y); # rotate around y axis
      for ($what) {
	(/r/ || /1/) && do {
	  $rho += $qty ;
	};
	(/t/ || /2/) && do {
	  $qty = deg2rad($qty);
	  $theta += $qty;
	};
	(/p/ || /3/) && do {
	  $qty = deg2rad($qty);
	  $phi += $qty;
	};
      };
      ($x, $z, $y) = spherical_to_cartesian($rho, $theta, $phi);
      $self->{camera} = [ $x, $y, $z ];
      last;
    };
  }
}

sub lookat {
  my $self = shift;
  if (@_) { $self->{lookat} = [@_] }
  return @{ $self->{lookat} };
}

######################################################
# end of field access functions
######################################################

sub load {
}

sub display {
  my $ldraw;
  $self->prepare_display;
  unless (eval { $ldraw = ${ $self->{ldraw} }->copy }) {
    glutSwapBuffers();
    return;
  };
  glutSwapBuffers() unless $self->{count};

  my @parts = @{$ldraw};
  local $, = " "; local $\ = "\n";

  $ldraw->build_gl_tree unless $self->{changed};

  for my $part (@parts) {
    next unless $part->type;
    if ($part->type == 1) {
      $self->display_part($part);
    } else {
      $self->display_primitive($part);
    }
#    glutSwapBuffers() unless $self->{count};
  }
  glutSwapBuffers();
  $self->{changed}++;
  $self->{count}++;
}

use Data::Dumper;
sub build_list {
  shift;
  my $part = shift;
  unless (ref $part) {
    $part = Lego::Ldraw::Line->new_from_part_name($part);
    $part->model(${$self->{ldraw}});
  }
  my $lcolor = shift; $lcolor = $part->color unless defined $lcolor;
  return if defined $self->{GL_LISTS}->{$part->name}->{$lcolor};

  my $data = $part->explode->display_struct;

  #-------------------------------------------
  # first, build lists for all colored parts
  #-------------------------------------------
  if ($data->{1}) {
    for my $line (@{$data->{1}->{16}}) {
      $self->build_list($line, $lcolor)
    }
  }

  #-------------------------------------------
  # then start generating the list
  #-------------------------------------------
  my $ln = glGenLists(1);

  return unless $ln;
  glNewList($ln, GL_COMPILE);

  #-------------------------------------------
  # with subparts...
  #-------------------------------------------
  if ($data->{1}) {
    for my $color (keys %{$data->{1}}) {
      for my $line (@{$data->{1}->{$color}}) {
	my $col = $color == 16 ? $lcolor : $color;
	glColor4f(gl_color($col));
	$self->display_part($line, $col);
      }
    }
  }

  #-------------------------------------------
  # ...and primitives
  #-------------------------------------------
  for my $type (2, 3, 4) {
    for my $color (keys %{$data->{$type}}) {
      for my $line (@{$data->{$type}->{$color}}) {
	$self->display_primitive($line);
      }
    }
  }
  glEndList();

  $self->{GL_LISTS}->{$part->name}->{16} = $ln;
  return $ln;
}

sub display_part {
  shift;

  my $part = shift;
  my $col = shift; $col = $part->color unless defined $col;

  #-------------------------------------------
  # first, check if a display list is
  # defined, and define one if not so...
  #-------------------------------------------
  my $ln = $self->{GL_LISTS}->{$part->name}->{$part->color};
  $self->{GL_LISTS}->{$part->name}->{$part->color} = $self->build_list($part)
    unless (defined $ln);
  $ln = $self->{GL_LISTS}->{$part->name}->{$part->color};

  #-------------------------------------------
  # ...then call list
  #-------------------------------------------
  glPushMatrix();
  my @matrix = ($part->values(qw/a d g '' b e h '' c f i '' x y z/), 1);
  glMultMatrixd_p(@matrix);
  glCallList($ln);
  glPopMatrix();
}

sub display_primitive {
  shift;
  my $part = shift;
  my $gl_type;
  my @d = $part->values;

  for ($part->type) {
    /^5$/ && do { return; $gl_type = GL_LINES;     @d = splice @d, 2, 6; last; };
    /^2$/ && do { return; $gl_type = GL_LINES;     splice @d, 0, 2; last; };
    /^3$/ && do { $gl_type = GL_TRIANGLES; splice @d, 0, 2; last; };
    /^4$/ && do { $gl_type = GL_QUADS;     splice @d, 0, 2; last; };
  }

  glBegin($gl_type);

  #-----------------------------------------------
  # normals are supposed to be important for
  # lighting, but I can't manage to make lighting
  # work, and besides I get divisions by zero
  #-----------------------------------------------
  #my @normal = $part->normal;
  #print @normal;
  #glNormal3f(@normal)
  #  if (($part->type == 3) or ($part->type == 4));
  #-----------------------------------------------
  # end of normal handling, here for the future
  #-----------------------------------------------

  while (@d) {
    my @c = splice @d, 0, 3;
    carp "Wrong part $part" unless (scalar @c == 3);
    glVertex3f(@c);
  }
  glEnd();
}

##########################################################
# initialization etc.
##########################################################
sub resize {
  my ($width, $height) = @_;

  # Let's not core dump, no matter what.
  $height = 1 if ($height == 0);

  glViewport(0, 0, $width, $height);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(45.0,$width/$height,0.1,4000.0);

  glMatrixMode(GL_MODELVIEW);

  $self->{width} = $width;
  $self->{height} = $height;
}

sub _specialkey {
  my $key = shift;
  my $mod = glutGetModifiers();

  return unless defined $self->{specialkeypressed}->{$mod};
  return unless $self->{specialkeypressed}->{$mod}->{$key};

  my $sub = $self->{specialkeypressed}->{$mod}->{$key};
  &$sub($self);
}

sub bindspec {
  shift;
  my $key = shift;
  my $sub = pop;
  my $mod = shift || 0;
  $self->{specialkeypressed}->{$mod}->{$key} = $sub;
}

sub init {
  glutInit();
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowSize($self->{width}, $self->{height});

  $self->{id} = glutCreateWindow($self->{name});

  glutDisplayFunc(\&display);
  glutIdleFunc(\&display);
  glutReshapeFunc(\&resize);
  glutSpecialFunc(\&_specialkey);

  #($self->{width}, $self->{height}) = (600, 800);
  ourInit($self->{width}, $self->{height});
  #glutIdleFunc();
  glutMainLoop();
}

sub ourInit {
  my ($Width, $Height) = @_;

  # Color to clear color buffer to.
  glClearColor(@{$self->{clearcolor}});

  # Depth to clear depth buffer to; type of test.
  glClearDepth(1.0);
  glDepthFunc(GL_LESS);

  # Enables Smooth Color Shading; try GL_FLAT for (lack of) fun.
  glShadeModel(GL_SMOOTH);
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);

  # Load up the correct perspective matrix; using a callback directly.
  resize($self->{width}, $self->{height});

  # Set up a light, turn it on.
  glLightfv_p(GL_LIGHT1, GL_POSITION, @{$self->{light_position}});
  glLightfv_p(GL_LIGHT1, GL_AMBIENT,  @{$self->{light_ambient}});
  glLightfv_p(GL_LIGHT1, GL_DIFFUSE,  @{$self->{light_diffuse}});
  glEnable (GL_LIGHT1);

  # A handy trick -- have surface material mirror the color.

  glColorMaterial(GL_FRONT_AND_BACK,GL_AMBIENT_AND_DIFFUSE);
  #glColorMaterial(GL_FRONT_AND_BACK, GL_SPECULAR);
  glEnable(GL_COLOR_MATERIAL);
}

sub prepare_display {
  shift;
  glDisable(GL_TEXTURE_2D);
  glDisable(GL_LIGHTING);
  #glEnable(GL_LIGHTING);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glEnable(GL_BLEND);
  glEnable(GL_DEPTH_TEST);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  # Need to manipulate the ModelView matrix to move our model around.
  glMatrixMode(GL_MODELVIEW);

  # Reset to 0,0,0; no rotation, no scaling.
  glLoadIdentity();

  # Move the object back from the screen.
  glTranslatef(0.0, 0.0, 0);
  # move the camera away
  gluLookAt($self->camera, $self->lookat, 0, -1, 0);

  # Clear the color and depth buffers.
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

##########################################################
# color handling
##########################################################

my $color_matrix =
  {
   117440511 => [qw/153 192 240 148/],
   0 => [qw/34 34 34/],
   3 => [qw//],
   431 => [qw//],
   379 => [qw/159 178 191/],
   10 => [qw/51 255 102/],
   33 => [qw/0 0 153/],
   46 => [qw/240 196 0/],
   32 => [qw//],
   335 => [qw/212 163 157/],
   378 => [qw/159 204 180/],
   462 => [qw//],
   5 => [qw/255 51 153/],
   42 => [qw/204 255 0/],
   382 => [qw/204 170 102/],
   383 => [qw/204 204 204/],
   418 => [qw/0 191 89/],
   495 => [qw/255 255 128/],
   6 => [qw/102 51 0/],
   14 => [qw/255 229 0/],
   503 => [qw/230 227 218/],
   4 => [qw/204 0 0/],
   373 => [qw/175 150 180/],
   1 => [qw/0 51 178/],
   9 => [qw/0 128 255/],
   3 => [qw/48 128 48/],
   5 => [qw/255 51 153/],
   334 => [qw/240 176 51/],
   12 => [qw/255 201 196/],
   11 => [qw/48 255 48/],
   494 => [qw/204 204 204/],
   2 => [qw/0 127 51/],
   383 => [qw/204 204 204/],
   36 => [qw/204 0 0/],
   15 => [qw/255 255 255/],
   41 => [qw/153 192 240/],
   34 => [qw/0 80 24/],
   13 => [qw/255 176 204/],
   47 => [qw/255 255 255/],
   7 => [qw/153 153 153/],
   8 => [qw/102 102 88/],
  };


sub gl_color {
  my $col      = shift;
  my $linetype = shift;

  $col = 0 unless defined $color_matrix->{$col};
  my @color = @{$color_matrix->{$col}}; $_ /= 256 for @color;
  @color = (@color, 1) unless scalar @color == 4;
  return (@color);
}


sub Lego::Ldraw::Line::gl_color {
  my $self = shift;
  my $ld = shift;   # line or colour
  my $tp = shift; # false if triang or quad (types 3 and 4) , true if line (types 2 and 5)

  $ld = ref $ld ? $ld->color : $ld;
  $tp = ref $ld ? (($ld->type != 3) && ($ld->type != 4)) : $tp;

}

sub Lego::Ldraw::build_gl_tree {
  my $self = shift;
  my $callback = sub { Lego::Ldraw::Display->build_list( shift ) };
  $self->build_tree($callback);
}

sub Lego::Ldraw::display_struct {
  my $self = shift;
  my $data;

  for my $line (@$self) {
    if ($line->type == 5) {
      $line = $line->five2two
    }
    push @{$data->{$line->type}->{$line->color}}, $line
	if $line->type;
  }
  return $data;
}

sub Lego::Ldraw::Line::normal {
  my $self = shift;
  return unless $self->type == 3 or $self->type == 4;
  my @d = $self->values;
  splice @d, 0, 2;
  my ($a, $b, $c, $d, $n);
  for ($a, $b, $c) {
    $_ = vector(splice @d, 0, 3);
  }

  ($n, $d) = plane($a, $b, $c);
  return $n->array;
}

sub Lego::Ldraw::Line::five2two {
  my $self = shift;
  return $self unless $self->type == 5;

  $self = $self->copy;
  $self->{type} = 2;
  for (qw/x3 y3 z3 x4 y4 z4/) {
    delete $self->{$_}
  }
  return $self;
}

1;
