##########################################################################
# This file is part of Vacuum Magic
# Copyright (C) 2008 by UPi <upi at sourceforge.net>
##########################################################################


##########################################################################
package Texture;
##########################################################################

=comment
The Texture objects are the basic building blocks in Vacuum Magic.
They work in conjunction with the Sprite and GlFont object to provide the visuals.

Textures are loaded from graphics files on demand.
Creating a new texture with the new() sub will only declare it,
it will be loaded when it is first Blitted.

To conserve video memory, Textures can be unloaded at certain times.
Some textures are permanent (the often-used textures), they will never unload.
Other textures are transient (like Boss textures or Background textures).
They will unload when UnloadTransientTextures() is called.
=cut



use SDL::OpenGL;
use vars qw( $FreeTextureIDs $CurrentTexture );
use strict;
$CurrentTexture = 0;
$FreeTextureIDs = [];

sub new {
  my ($class, $name, $filename, $depth, $keepSurface) = @_;
  
  die "Texture '$name' already exists"  if $::Textures{$name};
  die "File '$filename' doesn't exist"  if $filename && ! -r $filename;
  my $self = {
    id => undef,
    name => $name,
    filename => $filename,
    w => 0,
    h => 0,
    fullw => 0,
    fullh => 0,
    depth => ($depth || 0),
    keepSurface => ($keepSurface || 0),
    attributes => {},
    loaded => 0,
    permanent => 0,
  };
  bless $self, $class;
  $::Textures{$name} = $self;
  return $self;
}


# = SETTING TEXTURE ATTRIBUTES ===========================================

sub SetPermanent {
  my ($self) = @_;
  
  $self->{permanent} = 1;
  $self->LoadFromFile();
}

sub SetRepeatX {
  my ($self) = @_;
  
  $self->{attributes}->{RepeatX} = \&_InternalSetRepeatX;
  $self->_InternalSetRepeatX()  if $self->{loaded};
}

sub _InternalSetRepeatX {
  my ($self) = @_;
  
  $self->_Bind();
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
}

sub SetRepeatY {
  my ($self) = @_;
  
  $self->{attributes}->{RepeatY} = \&_InternalSetRepeatY;
  $self->_InternalSetRepeatY()  if $self->{loaded};
}

sub _InternalSetRepeatY {
  my ($self) = @_;
  
  $self->_Bind();
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}

sub DisableAntialiasing {
  my ($self) = @_;
  
  $self->{attributes}->{DisableAntialiasing} = \&_InternalDisableAntialiasing;
  $self->_InternalDisableAntialiasing()  if $self->{loaded};
}

sub _InternalDisableAntialiasing {
  my ($self) = @_;
  
  $self->_Bind();
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); # scale linearly when image bigger than texture
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); # scale linearly when image smalled than texture
}


# = LOADING / UNLOADING THE TEXTURE ======================================

sub LoadFromFile {
  my ($self) = @_;
  my ($surface);
  
  return  if $self->{loaded};
  $surface = new SDL::Surface(-name => $self->{filename});
  $self->_LoadFromSurface($surface);
}

sub LoadFromSurface {
  my ($self, $surface) = @_;
  
  Carp::confess("Texture is already loaded: $self->{name}")  if $self->{loaded};
  $self->{surface} = $surface;
  $self->{permanent} = 1;  # We don't unload textures that were hand-created and not loaded from a file.
  $self->_LoadFromSurface($self->{surface});
}

sub _LoadFromSurface {
  my ($self, $surface) = @_;
  
  Carp::confess("_LoadFromSurface: No surface")  unless $surface;
  Carp::confess("_LoadFromSurface: Texture $self->{name} already has an id")  if defined($self->{id});
  $self->{id} = &_GetTextureID();
  warn "Loading Texture $self->{name} ID $self->{id}";
  $self->{w} = $surface->width;
  $self->{h} = $surface->height;
  if ( $surface->bytes_per_pixel < 3 or !&_IsPowerOf2($surface->width) or !&_IsPowerOf2($surface->height)) {
    $surface = &_ResizeSurface($surface);
  }
  $self->{surface} = $surface  if $self->{keepSurface};
  $self->{fullw} = $surface->width;
  $self->{fullh} = $surface->height;

  $self->_Bind();
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); # scale linearly when image bigger than texture
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); # scale linearly when image smalled than texture
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
  &glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
  
  &glTexImage2D( GL_TEXTURE_2D,
                 0,                      # level (0 normal, heighr is form mip-mapping)
                 $surface->bytes_per_pixel() == 3 ? GL_RGB : GL_RGBA,                # internal format (3=GL_RGB)
                 $surface->width, $surface->height,
                 0,                      # border 
                 $surface->bytes_per_pixel() == 3 ? GL_RGB : GL_RGBA,                # format RGB color data
                 GL_UNSIGNED_BYTE,       # unsigned bye data
                 $surface->pixels );      # ptr to texture data
  &::CheckGLError();
  $self->{loaded} = 1;
  $self->_ApplyAttributes();
}

sub _GetTextureID {
  if (@$FreeTextureIDs) {
    die "Could not genereate textures" unless $$FreeTextureIDs[0];
    return shift @$FreeTextureIDs;
  }
  $FreeTextureIDs = &::glGenTextures(10);
  die "Could not genereate textures" unless $$FreeTextureIDs[0];
  return shift @$FreeTextureIDs;
}

sub _IsPowerOf2 {
  my ($number) = @_;
  
  return 0  if $number < 1;
  while (1) {
    if ($number & 1) {
      return $number == 1;
    }
    $number >>= 1;
  }
}

sub _ResizeSurface {
  my ($surface) = @_;
  my ($width, $height, $resizedSurface);
  
  $width = 1; while ($width < $surface->width) { $width <<= 1; }
  $height = 1; while ($height < $surface->height) { $height <<= 1; }
  warn 'Resizing ', $surface->width, ' x ', $surface->height, " to $width x $height";
  $resizedSurface = new SDL::Surface( -w => $width, -h => $height, -d => 32, -flags=>::SDL_SWSURFACE(), -name => '' );
  $surface->set_alpha(0, 255);
  $surface->blit(0, $resizedSurface, 0);
  return $resizedSurface;
}

sub _ApplyAttributes {
  my ($self) = @_;
  
  foreach (values %{$self->{attributes}}) {
    $_->($self);
  }
}

sub UnloadTransientTextures {
  my @texturesToUnload = grep { $_->{loaded} and not($_->{permanent}) } values %::Textures;
  &_UnloadTextureList(@texturesToUnload);
}

sub UnloadAllTextures {
  my @texturesToUnload = grep { $_->{loaded} } values %::Textures;
  &_UnloadTextureList(@texturesToUnload);
}

sub LoadPermanentTextures {
  my @texturesToLoad = grep { $_->{permanent} and not $_->{loaded} } values %::Textures;
  foreach (@texturesToLoad) {
    if ($_->{surface}) {
      $_->_LoadFromSurface($_->{surface});
    } else {
      $_->LoadFromFile();
    }
  }
}

sub _UnloadTextureList {
  my (@texturesToUnload) = @_;
  my (@textureIds);
  
  return  unless @texturesToUnload;
  @textureIds = map { $_->{id} } @texturesToUnload;
  
  warn "Unloading: ", join(':', map { $_->{name} } @texturesToUnload), "; id: @textureIds";
  &glDeleteTextures(@textureIds);
  &::CheckGLError();
  foreach (@texturesToUnload) {
    $CurrentTexture = ''  if $CurrentTexture eq $_;
    $_->{loaded} = 0;
    unshift @$FreeTextureIDs, $_->{id};   # This texture ID can now be reused.
    $_->{id} = undef;
  }
}


# = LOADING / UNLOADING THE TEXTURE ======================================

sub Blit {
  my ($self, $screenX, $screenY, $w, $h, $textureX, $textureY, $textureW, $textureH) = @_;
  my ($tx1, $ty1, $tx2, $ty2);
  
  $self->Bind();
  
  $screenX = 0     unless $screenX;
  $screenY = 0     unless $screenY;
  $w = $self->{w}  unless $w;
  $h = $self->{h}  unless $h;
  $textureX = 0    unless $textureX;
  $textureY = 0    unless $textureY;
  $textureW = $w   unless $textureW;
  $textureW = $self->{w}  if $textureW == -1;
  $textureH = $h   unless $textureH;
  $textureH = $self->{h}  if $textureH == -1;
  
  if ($w < 0) {
    $w = -$w;
    $textureW = -$textureW;
  }
  if ($textureW < 0) {
    $textureX -= $textureW;  # Horizontal flip
  }
  if ($h < 0) {
    $h = -$h;
    $textureH = -$textureH;
  }
  if ($textureH < 0) {
    $textureY -= $textureH;  # Vertical flip
  }
  
  $tx1 = $textureX / $self->{fullw};
  $ty1 = $textureY / $self->{fullh};
  $tx2 = ($textureX + $textureW) / $self->{fullw};
  $ty2 = ($textureY + $textureH) / $self->{fullh};

  &glBegin(GL_QUADS);
    &glTexCoord($tx1, $ty2); &glVertex($screenX, $screenY, $self->{depth});
    &glTexCoord($tx1, $ty1); &glVertex($screenX, $screenY + $h, $self->{depth});
    &glTexCoord($tx2, $ty1); &glVertex($screenX + $w, $screenY + $h, $self->{depth});
    &glTexCoord($tx2, $ty2); &glVertex($screenX + $w, $screenY, $self->{depth});
  &glEnd();
}

sub RotoBlit {
  my ($self, $screenX, $screenY, $screenW, $screenH, $rotation) = @_;
  
  $self->Bind();
  my @textureCoords = ([0,1], [0,0], [1,0], [1,1]);
  
  &glBegin(GL_QUADS);
    &glTexCoord(@{$textureCoords[$rotation++ % 4]}); &glVertex($screenX, $screenY, $self->{depth});
    &glTexCoord(@{$textureCoords[$rotation++ % 4]}); &glVertex($screenX, $screenY + $screenH, $self->{depth});
    &glTexCoord(@{$textureCoords[$rotation++ % 4]}); &glVertex($screenX + $screenW, $screenY + $screenH, $self->{depth});
    &glTexCoord(@{$textureCoords[$rotation++ % 4]}); &glVertex($screenX + $screenW, $screenY, $self->{depth});
  &glEnd();
}

sub Bind {
  my ($self) = @_;
  
  $self->LoadFromFile()  unless $self->{loaded};
  $self->_Bind();
}

sub _Bind {
  my ($self) = @_;
  
  # The texture is assumed to be loaded or being loaded.
  Carp::confess("_Bind called for unloaded texture: $self->{name}")  unless $self->{id};
  return  if $CurrentTexture eq $self;
  $CurrentTexture = $self;
  &glBindTexture(GL_TEXTURE_2D, $self->{id});
}

