//
// base class of polygon set.
// derived from haXe molehill example written by Nicolas Cannasse
//

import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.GradientType;

import flash.display3D.IndexBuffer3D;
import flash.display3D.VertexBuffer3D;
import flash.display3D.Context3D;
import flash.display3D.textures.Texture;

import flash.Vector;
import flash.geom.Vector3D;
import flash.geom.Matrix;
import flash.geom.Matrix3D;

import flash.errors.Error;
import flash.errors.RangeError;

class Polygon {
  // contain vertex pos and normal
  public var verts( __getVertex, __setVertex ):Array< Vertex >;
  public var faces( __getFaces, __setFaces ):Array< Face >;
  public var origin( __getOrigin, __setOrigin ):Point3D;
  public var direction( __getDirection, __setDirection ):Point3D;

  public var ibuf( __getIndexBuffer, null ):IndexBuffer3D;
  public var vbuf( __getVertexBuffer, null ):VertexBuffer3D;

  public var texture( __getTexture, __setTexture ):Texture;
  private var __shader( null, null ):Dynamic;
  private var __color( null, null ):Vector3D;
  private var __alpha( null, null ):Float;

  // light params
  public var ambient( __getAmbient, __setAmbient ):Float;
  public var diffuse( __getDiffuse, __setDiffuse ):Float;
  public var specular( __getSpecular, __setSpecular ):Float;
  public var gloss( __getGloss, __setGloss ):Float;

  public function new() {
    clear();
  }

  public function clear():Void {
    verts = new Array< Vertex >();
    faces = new Array< Face >();
    origin = new Point3D( 0.0, 0.0, 0.0 );
    direction = new Point3D( 0.0, 1.0, 0.0 );

    // tentative values
    ambient = 0.4;
    diffuse = 0.5;
    specular = 0.2;
    gloss = 30;
  }

  private function dispose() {
    if ( ibuf != null ) {
      ibuf.dispose();
      ibuf = null;
    }
    if ( vbuf != null ) {
      vbuf.dispose();
      vbuf = null;
    }
  }

  public function addVertexPos( p:Point3D ):Int {
    return( verts.push( new Vertex( p ) ) );
  }

  public function addVertex( v:Vertex ):Int {
    return( verts.push( v.clone() ) );
  }

  public function addFace( f:Face ) {
    faces.push( f );
  }

  // setter and getter, registered for debugging
  public function __getVertex():Array< Vertex > { return( verts ); }
  public function __getFaces():Array< Face > { return( faces ); }
  public function __getOrigin():Point3D { return( origin ); }
  public function __getDirection():Point3D { return( direction ); }
  public function __getIndexBuffer():IndexBuffer3D { return( ibuf ); }
  public function __getVertexBuffer():VertexBuffer3D { return( vbuf ); }
  public function __getTexture():Texture { return( texture ); }
  public function __getAmbient():Float { return( ambient ); }
  public function __getDiffuse():Float { return( diffuse ); }
  public function __getSpecular():Float { return( specular ); }
  public function __getGloss():Float { return( gloss ); }

  public function __setVertex( va:Array< Vertex > ):Array< Vertex > {
    verts = va;
    return( verts );
  }
  public function __setFaces( fs:Array< Face > ):Array< Face > {
    faces = fs;
    return( faces );
  }
  public function __setOrigin( o:Point3D ):Point3D {
    origin = o;
    return( origin );
  }
  public function __setDirection( d:Point3D ):Point3D {
    direction = d;
    direction.normalize();
    return( direction );
  }
  public function __setTexture( t:Texture ):Texture {
    texture = t;
    return( texture );
  }
  public function __setAmbient( a:Float ):Float {
    ambient = a;
    return( ambient );
  }
  public function __setDiffuse( d:Float ):Float {
    diffuse = d;
    return( diffuse );
  }
  public function __setSpecular( s:Float ):Float {
    specular = s;
    return( s );
  }
  public function __setGloss( g:Float ):Float {
    gloss = g;
    return( gloss );
  }

  public function getVertex( i:Int ):Vertex { return( verts[i] ); }
  public function getFace( i:Int ):Face { return( faces[i] ); }

  private function setShader( c:Context3D,
                              str:String ):Void {
    switch( str ) {
      case "2D":
        __shader = new Shader2D( c );
      case "2DUV":
        __shader = new Shader2DUV( c );
      case "Simple":
        __shader = new SimpleShader( c );
      case "SimpleUV":
        __shader = new SimpleShaderUV( c );
      case "Gouraud":
        __shader = new GouraudShader( c );
      case "GouraudUV":
        __shader = new GouraudShaderUV( c );
      case "Phong":
        __shader = new PhongShader( c );
      case "PhongUV":
        __shader = new PhongShaderUV( c );
      default:
        trace( "invalid shader name : " + str );
        trace( "available shaders are : Simple, Gouraud, GouraudUV, Phong, PhongUV");
        trace( "tentatively assigned to Gouraud shader." );
        __shader = new GouraudShader( c );
    }
  }

  public function allocate( c:Context3D,
                            ?str:String = "Gouraud",
                            ?color:Int = 0x00FF00,
                            ?alpha:Float = 1.0 ):Void {
    var hasNormal:Bool = false;
    var hasUV:Bool = false;
    __alpha = alpha;

    setShader( c, str ); // construct shader
    dispose();
    // Type.getClassName fails, why?
    var d:Dynamic = Type.getClass( this.__shader );
    var cn:String = d.id;
    if ( cn != "SimpleShader" &&
         cn != "SimpleShaderUV" &&
         cn != "2DShader" &&
         cn != "2DShaderUV" ) { // all the other shaders use normal vector
      addVertexNormals();
      hasNormal = true;
    }
    if ( cn == "GouraudShaderUV" || cn == "PhongShaderUV" ||
         cn == "2DShaderUV" || cn == "SimpleShaderUV" ) hasUV = true;

    // set color or texture
    // create single colored texture
    if ( cn == "GouraudShaderUV" || cn == "PhongShaderUV" ) {
      createSingleColoredTexture( c, color );
    } else if ( cn == "GouraudShader" || cn == "PhongShader" ||
                cn == "SimpleShader" ) {
      // split color into three float values
      var r:Float = cast( ( color & 0xFF0000 ) >> 16, Float ) / 255.0;
      var g:Float = cast( ( color & 0x00FF00 ) >> 8, Float ) / 255.0;
      var b:Float = cast( ( color & 0x0000FF ), Float ) / 255.0;
      __color = new Vector3D( r, g, b );
    }

    var numIndex:Int = faces.length * 3;
    var size:Int = 3;
    if ( hasNormal ) size += 3;
    if ( cn == "SimpleShader" ) size += 3; // for color
    ibuf = c.createIndexBuffer( numIndex );
    if ( hasUV ) {
      size += 2;
      ibuf.uploadFromVector( generateIndexes( numIndex ), 0, numIndex );
    } else {
      ibuf.uploadFromVector( generateIndexes(), 0, numIndex );
    }
    var buf:Vector< Float > = new Vector< Float >();
    var myIndex = 0;
    if ( !hasUV ) { // do not use UV-mapping, upload vertex and its normal
      myIndex = verts.length;
      for ( vert in verts ) {
        buf.push( vert.pos.x );
        buf.push( vert.pos.y );
        buf.push( vert.pos.z );
        if ( hasNormal ) {
          buf.push( vert.normal.x );
          buf.push( vert.normal.y );
          buf.push( vert.normal.z );
        } else if ( cn == "SimpleShader" ) {
          buf.push( __color.x );
          buf.push( __color.y );
          buf.push( __color.z );
        }
      }
    } else {
      myIndex = numIndex;
      if ( cn == "2DShaderUV" || cn == "SimpleShaderUV" ) {
        for ( face in faces ) {
          addToBuffer2D( buf, face.ids.i, face.uv0 );
          addToBuffer2D( buf, face.ids.j, face.uv1 );
          addToBuffer2D( buf, face.ids.k, face.uv2 );
        }
      } else {
        if ( !hasNormal || !hasUV ) {
          trace( "Polygon::allocate - not extepected operation." );
          dispose();
          return;
        }
        for ( face in faces ) {
          addToBuffer( buf, face.ids.i, face.uv0 );
          addToBuffer( buf, face.ids.j, face.uv1 );
          addToBuffer( buf, face.ids.k, face.uv2 );
        }
      }
    }
    vbuf = c.createVertexBuffer( myIndex, size );
    vbuf.uploadFromVector( buf, 0, myIndex );
  }

  private function createSingleColoredTexture( c:Context3D,
                                               color:Int ):Void {
    var size:Int = 64;
    var sp:Sprite = new Sprite();
    sp.graphics.beginFill( color );
    sp.graphics.drawRect( 0, 0, size, size );
    sp.graphics.endFill();
    var bd:BitmapData = new BitmapData( size, size );
    bd.draw( sp );
    texture = c.createTexture( size, size, flash.display3D.Context3DTextureFormat.BGRA, false );
    texture.uploadFromBitmapData( bd );
  }

  private function __drawDoubleColoredSprite( color0:Int,
                                              color1:Int,
                                              ?size:Int = 64 ):Sprite {
    var sp:Sprite = new Sprite();
    sp.graphics.beginFill( color0 );
    sp.graphics.drawRect( 0, 0, size / 2, size );
    sp.graphics.endFill();
    sp.graphics.beginFill( color1 );
    sp.graphics.drawRect( size / 2, 0, size / 2, size );
    sp.graphics.endFill();
    return( sp );
  }

  public function createDashedDoubleColoredTexture( c:Context3D,
                                                    d:Int,
                                                    color0:Int,
                                                    color1:Int,
                                                    size:Int = 256 ):Void {
    var sp:Sprite = __drawDoubleColoredSprite( color0, color1, size );
    var bd:BitmapData = new BitmapData( size, size, true, 0x00FFFFFF );
    bd.draw( sp );
    // strange magic number appears ...
    var ebw:Int = Std.int( 0.8 * bd.width / ( d * 2 + 1 ) );
    var offset:Int = Std.int( 0.1 * bd.width );
    for ( i in 0 ... d ) {
      var myx:Int = ebw * ( 2 * i + 1 );
      var mynextx:Int = ebw * 2 * ( i + 1 );
      for ( j in myx ... mynextx ) {
        for ( k in 0 ... bd.height ) {
          bd.setPixel32( j + offset, k, 0x00FFFFFF );
        }
      }
    }
    texture = c.createTexture( size, size, flash.display3D.Context3DTextureFormat.BGRA, false );
    texture.uploadFromBitmapData( bd );
  }

  public function createDoubleColoredTexture( c:Context3D,
                                              color0:Int,
                                              color1:Int,
                                              ?size:Int = 64 ):Void {
    var sp:Sprite = __drawDoubleColoredSprite( color0, color1, size );
    var bd:BitmapData = new BitmapData( size, size );
    bd.draw( sp );
    texture = c.createTexture( size, size, flash.display3D.Context3DTextureFormat.BGRA, false );
    texture.uploadFromBitmapData( bd );
  }

  private function __drawGradientSprite( color0:Int,
                                         color1:Int,
                                         ?size:Int = 64 ):Sprite {
    var sp:Sprite = new Sprite();
    var mat:Matrix = new Matrix();
    mat.createGradientBox( size, size, 0, 0, 0 );
    sp.graphics.beginGradientFill( GradientType.LINEAR, [ color0, color1 ], [ 1.0, 1.0 ], [ 30, 225 ], mat );
    sp.graphics.drawRect( 0, 0, size, size );
    sp.graphics.endFill();
    return( sp );
  }

  public function createGradientTexture( c:Context3D,
                                         color0:Int,
                                         color1:Int,
                                         ?size:Int = 64 ):Void {
    var sp:Sprite = __drawGradientSprite( color0, color1, size );
    var bd:BitmapData = new BitmapData( size, size );
    bd.draw( sp );
    texture = c.createTexture( size, size, flash.display3D.Context3DTextureFormat.BGRA, false );
    texture.uploadFromBitmapData( bd );
  }

  private function addToBuffer2D( buf:Vector< Float >,
                                  n:Int,
                                  uv:UVCoord ):Void {
    buf.push( verts[n].pos.x );
    buf.push( verts[n].pos.y );
    buf.push( verts[n].pos.z );
    buf.push( uv.u );
    buf.push( uv.v );
  }

  private function addToBuffer( buf:Vector< Float >,
                                n:Int,
                                uv:UVCoord ):Void {
    buf.push( verts[n].pos.x );
    buf.push( verts[n].pos.y );
    buf.push( verts[n].pos.z );
    buf.push( verts[n].normal.x );
    buf.push( verts[n].normal.y );
    buf.push( verts[n].normal.z );
    buf.push( uv.u );
    buf.push( uv.v );
  }

  private function generateIndexes( ?num:UInt ):Vector< UInt > {
    var vec:Vector< UInt > = new Vector< UInt >();
    if ( num == null ) {
      for ( face in faces ) {
        vec.push( face.ids.i );
        vec.push( face.ids.j );
        vec.push( face.ids.k );
      }
    } else {
      for ( i in 0 ... num ) vec.push( i );
    }
    return( vec );
  }

  public function addVertexNormals():Void {
    for ( face in faces ) { // loop over faces
      var ids:TriFaceIDs = face.ids;
      if ( !ids.isValid() ) {
        trace( "skip invalid face: " + ids.toString() );
        continue;
      }
      var nm:Point3D = getFaceNormal( ids );
      verts[ids.i].addNormal( nm );
      verts[ids.j].addNormal( nm );
      verts[ids.k].addNormal( nm );
    }
    // normalize
    for ( vert in verts ) {
      vert.normal.normalize();
    }
  }

  // return normal vector of face[id]
  private function getFaceNormal( ids:TriFaceIDs ):Point3D {
    var v0:Point3D = verts[ids.j].pos.clone().sub( verts[ids.i].pos );
    var v1:Point3D = verts[ids.k].pos.clone().sub( verts[ids.i].pos );
    return( v0.cross( v1 ) );
  }

  public function translate( x:Float,
                             y:Float,
                             z:Float ):Void {
    for ( vert in verts ) {
      vert.pos.x += x;
      vert.pos.y += y;
      vert.pos.z += z;
    }
    origin.x += x;
    origin.y += y;
    origin.z += z;
  }

  public function translateByVec( p:Point3D ):Void {
    translate( p.x, p.y, p.z );
  }

  // rotation around axis by angle degree
  // origin of the rotation is or
  public function axisRotation( axis:Point3D,
                                or:Point3D,
                                angle:Float ):Void {
    for ( vert in verts ) vert.pos.sub( or );
    var ar:Point3D = axis.clone();
    ar.normalize();

    var Ri:Point3D = __genRot( ar, new Point3D( 1.0, 0.0, 0.0 ), angle );
    var Rj:Point3D = __genRot( ar, new Point3D( 0.0, 1.0, 0.0 ), angle );
    var Rk:Point3D = __genRot( ar, new Point3D( 0.0, 0.0, 1.0 ), angle );

    //     | Ri.x Rj.x Rk.x |
    // R = | Ri.y Rj.y Rk.y |
    //     | Ri.z Rj.z Rk.z |
    for ( vert in verts ) {
      var v:Point3D = vert.pos.clone();
      vert.pos.x = v.dot( Ri ) + or.x;
      vert.pos.y = v.dot( Rj ) + or.y;
      vert.pos.z = v.dot( Rk ) + or.z;
      if ( vert.normal != null ) {
        v = vert.normal;
        vert.normal.x = v.dot( Ri );
        vert.normal.y = v.dot( Rj );
        vert.normal.z = v.dot( Rk );
        vert.normal.normalize();
      }
    }
    for ( vert in verts ) vert.pos.add( or );
  }

  private function __genRot( axis:Point3D,
                             e:Point3D,
                             angle:Float ):Point3D {
    var o:Point3D = Point3D.getMultiply( axis, axis.dot( e ) );
    var oe:Point3D = Point3D.getSub( e, o );
    var uoe:Point3D = Point3D.getCross( axis, oe );
    return( o.clone().add( Point3D.getMultiply( oe, Math.cos( angle ) ) ).add( Point3D.getMultiply( uoe, Math.sin( angle ) ) ) );
  }

  public function draw( c:Context3D,
                        mpos:Matrix3D,
                        mproj:Matrix3D,
                        voffset:Vector3D,
                        ?light:Vector3D = null,
                        ?cpos:Vector3D = null ):Bool {
    var d:Dynamic = Type.getClass( __shader );
    var cn:String = d.id;
    switch( cn ) {
      case "2DShader":
        var vo:Vector3D = origin.toVector3D();
        __shader.init(
          { mpos:mpos, mproj:mproj, origin:vo },
          { color:__color, alpha:__alpha }
        );
      case "2DShaderUV":
        var vo:Vector3D = origin.toVector3D();
        __shader.init(
          { mpos:mpos, mproj:mproj, origin:vo },
          { tex:texture }
        );
      case "SimpleShader":
        __shader.init(
          { mpos:mpos, mproj:mproj, offset:voffset },
          { alpha:__alpha }
        );
      case "SimpleShaderUV":
        __shader.init(
          { mpos:mpos, mproj:mproj, offset:voffset },
          { tex:texture }
        );
      case "GouraudShader":
        __shader.init(
          { mpos:mpos, mproj:mproj, offset:voffset, light:light, ambient:ambient, diffuse:diffuse, col:__color },
          { alpha:__alpha }
        );
      case "GouraudShaderUV":
        __shader.init(
          { mpos:mpos, mproj:mproj, offset:voffset, light:light, ambient:ambient, diffuse:diffuse },
          { tex:texture, alpha:__alpha  }
        );
      case "PhongShader":
        __shader.init(
          { mpos:mpos, mproj:mproj, offset:voffset },
          { light:light, cpos:cpos, ambient:ambient, diffuse:diffuse, specular:specular, gloss:gloss, col:__color, alpha:__alpha }
        );
      case "PhongShaderUV":
        __shader.init(
          { mpos:mpos, mproj:mproj, offset:voffset },
          { tex:texture, light:light, cpos:cpos, ambient:ambient, diffuse:diffuse, specular:specular, gloss:gloss, alpha:__alpha }
        );
    }
    __shader.bind( vbuf );
    try {
      c.drawTriangles( ibuf );
      // finally is not implemented in haXe?
      __shader.unbind();
    } catch( e:Error ) {
      //trace( e.message );
      return( false );
    } catch( e:RangeError ) {
      //trace( e.message );
      return( false );
    }
    return( true );
  }

  private function addFaceRecursively0( q:Int,
                                        f:Face,
                                        r:Float,
                                        o:Point3D ):Void {
    var source:Array< Face > = new Array< Face >();
    var derived:Array< Face > = new Array< Face >();
    source.push( f );
    for ( i in 1 ... q ) {
      derived = [];
      for ( face in source ) {
        var nv12:Point3D = Point3D.getMiddle( verts[face.ids.j].pos,
                                              verts[face.ids.k].pos,
                                              true, r, o );
        var index12:Int = addVertexPos( nv12 ) - 1;
        var nv01:Point3D = Point3D.getMiddle( verts[face.ids.i].pos,
                                              verts[face.ids.j].pos,
                                              true, r, o );
        var index01:Int = addVertexPos( nv01 ) - 1;
        var nv20:Point3D = Point3D.getMiddle( verts[face.ids.k].pos,
                                              verts[face.ids.i].pos,
                                              true, r, o );
        var index20:Int = addVertexPos( nv20 ) - 1;
        var newuv12:UVCoord = UVCoord.getMiddle( face.uv1, face.uv2 );
        var newuv01:UVCoord = UVCoord.getMiddle( face.uv0, face.uv1 );
        var newuv20:UVCoord = UVCoord.getMiddle( face.uv2, face.uv0 );
        derived.push( new Face( face.ids.i, index01, index20,
                                face.uv0, newuv01, newuv20 ) );
        derived.push( new Face( index01, face.ids.j, index12,
                                newuv01, face.uv1, newuv12 ) );
        derived.push( new Face( index12, index20, index01,
                                newuv12, newuv20, newuv01 ) );
        derived.push( new Face( index20, index12, face.ids.k,
                                newuv20, newuv12, face.uv2 ) );
      }
      source = [];
      for ( face in derived ) source.push( face );
    }
    for ( face in source ) addFace( face );
  }

  // look at some direction
  // NOTE1: This function does not know the rotation of the scene.
  //        Do not use this after the rotation of the scene
  // TODO: release these restictions
  public function lookAt( p:Point3D ):Void {
    var cur:Point3D = new Point3D( 0, 1, 0 );
    var angle:Float = p.getAngle( cur );
    axisRotation( Point3D.getCross( p, cur ), origin, angle );
    direction = p.clone();
  }
}
