#!/usr/bin/perl
# ========================================================================
#< ac3dparse.pl - 18/05/2013
# AC3D parser and ...
# Based on ac3d2Horde3D format by Niek Albers, circa 13/11/2009
# 18/05/2013 geoff mclane http://geoffair.net/mperl
# TODO: 
# 1: There are several TODO items noted, mainly where a more strict 
#    parsing would help.
# 2: At present a rought fix in output to ensure a "faces" : [ ] index is 
#    not out of range of the vertices or normals lists, but should address
#    why this happens.
# 3: Should dig into the calculation of the normals. Original code expected 
#    only 3 refs, but have over-ridden this, but with what consequence?
# 4: Would be nice if some of the 'options' below were available on the command
#    line...
# ==========================================================================
my $os = $^O;
# This need to be adjusted to suit your environment
my $perl_dir = '/home/geoff/bin';
my $PATH_SEP = '/';
my $temp_dir = '/tmp';
if ($os =~ /win/i) {
    $perl_dir = 'C:\GTools\perl';
    $temp_dir = $perl_dir;
    $PATH_SEP = "\\";
}
unshift(@INC, $perl_dir);   # add location of lib_vec3.pm

my $VERSION = 0.8;  # 18/05/2013 - more fixes - geoff
#my $VERSION = 0.7;  # 12/05/2013 - lots of fixes - geoff
#my $VERSION = 0.6; # last by Niek Albers, circa 13/11/2009

use strict;
use warnings;
#no strict 'refs';
#no warnings;
use Data::Dumper;
use Carp;
use File::Copy;
# note: in the original this 'package' was included, but I prefer it separated
require 'lib_vec3.pm' or die "Unable to load 'lib_vec3.pm' Check paths in \@INC...\n";

my $pgmname = $0;
if ($pgmname =~ /(\\|\/)/) {
    my @tmpsp = split(/(\\|\/)/,$pgmname);
    $pgmname = $tmpsp[-1];
}

my $AC3D_FILE_VERSION = 11;

my $strict_triangluation = 0;   # 0=allow any number of SURF refs
my $json_file = '';

# options
my $show_objects = 0;       # turn off to reduce noise
my $show_calc_norms = 0;    # additional noise
my $show_parse_kid = 0;     # mainly for diagnostics
my $show_parse_numvert = 0; # likewise
my $show_out_of_range = 0;  # show when an index is 'fixed'
my $show_loc_vector = 0;    # show when parsing a 'loc x y z'

my $add_line_sep = 1;       # add extra line to the json
my $add_normals  = 1;       # generate normals

my ($g_minx,$g_miny,$g_minz,$g_maxx,$g_maxy,$g_maxz);
my $obj_count = 0;
my $vert_count = 0;

my $verbosity = 0;
sub VERB1() { return $verbosity >= 1; }
sub VERB2() { return $verbosity >= 2; }
sub VERB5() { return $verbosity >= 5; }
sub VERB9() { return $verbosity >= 9; }

my $scene_basename;

my $source = '';
my $ac3dfile;   # input file
my $jsonhand;
my @VERTICES;
my @INDICES;
my @NORMALS;

my @INDPERSURF = ();

my @warnings = ();
my $warn_count = 0; # also used as final exit level

my $index_offset   = 0;
my $batch_offset   = 0;
my $world          = {};
my $material_files = {};
my $last_name      = '';

# my $vertice_count = 0;

# 12/05/2013 - Added token 'subdiv' and 'texoff',
# and changed 'data' to what I understand it does
my $TOKENIZER = {
    'MATERIAL' => \&parse_material,
    'OBJECT'   => \&parse_object,
    'kids'     => \&parse_kids,
    'name'     => \&parse_name,
    'texture'  => \&parse_value,
    'crease'   => \&parse_value,
    'numvert'  => \&parse_numvert,
    'numsurf'  => \&parse_numsurf,
    'SURF'     => \&parse_surf,
    'mat'      => \&parse_mat,
    'refs'     => \&parse_refs,
    'texrep'   => \&parse_texrep,
    'rot'      => \&parse_rot,
    'loc'      => \&parse_loc,
    'url'      => \&parse_value,
    'data'     => \&parse_data,
    'subdiv'   => \&parse_value,
    'texoff'   => \&parse_texoff,
};

my $QuadBit = 1; # isBitSet( $type, 0 );
my $MatBit  = 2; # isBitSet( $type, 1 );
my $FUvBit  = 4; # isBitSet( $type, 2 );
my $VUvBit  = 8; # isBitSet( $type, 3 );
my $FNorms  = 16; # isBitSet( $type, 4 );
my $VNorms  = 32; # isBitSet( $type, 5 );
my $FColor  = 64; # isBitSet( $type, 6 );
my $VColor  = 128; # isBitSet( $type, 7 );

# ### DEBUG ###
# Just some vey specific 'test' files, so I can run the script from my editor
# with a blank command line... should be OFF for release
my $debug_on = 0;
my $def_file = 'C:\FG\18\blendac3d\shb\shbridge.ac';
#my $def_file = 'C:\FG\18\blendac3d\cube.ac';
#my $def_file = 'C:\FG\18\blendac3d\f16.ac';
#my $def_file = 'C:\FG\fgdata\Aircraft\A380\Models\FlightDeck\Glareshield\efis_c.ac'; # has 'subdiv 1'
#my $def_file = 'C:\FG\fgdata\Aircraft\a4\Models\attitude-mod1.ac'; # has 'texoff -0.5 0.5'
#14398: ac_to_gl: Unrecognised token 'light', line 'OBJECT light'
#my $def_file = 'C:\FG\fgdata\Aircraft\dhc8\Models\dhc8.ac';
#ac_to_gl: Illegal ref record. actually has several blank lines!!! could overlook, but NO!
# Also fails with blender ac importer until blank lines are removed
#my $def_file = 'C:\FG\fgdata\Aircraft\Dromader\Models\DropSystem.ac';
#my $def_file = 'C:\FG\ac3d\DropSystem.ac';  # fixed file - no warnings
############################################################################

eval { main(@ARGV) };
die($@) if ($@);
exit($warn_count);

sub prt($) { print shift; }
sub prtw($) { 
    my $t = shift; 
    push(@warnings,$t); 
    prt($t);
}

# simple vector3 class - now an external package
sub vec3 { vec3->new(@_) }

sub basename {
    my $path = shift;
    $path =~ s/.*[\/\\]//;
    return [ split( /\./, $path ) ]->[0];
}

sub filename {
    my $path = shift;
    $path =~ s/.*[\/\\]//;
    return $path;
}

# load a file line, and 'tokenise' it, removing
# any double quotes...
sub parse_line {
    my $line = <$ac3dfile>;
    return if ( !$line );
    my @items = ( $line =~ /(".*?"|\S+)/g );
    map { s/(^"|"$)//g } @items;
    return \@items;

}

# individual opcode parsers

sub parse_header {
    my $items = shift;
    $items->[0] =~ m/^AC3D(\w)/;
    die "not minimally an AC3D version $AC3D_FILE_VERSION file\n"
      if ( hex($1) < $AC3D_FILE_VERSION );
}

sub parse_roc {
    my $items = shift;
    return [
        [ $items->[1], $items->[2], $items->[3] ],
        [ $items->[4], $items->[5], $items->[6] ],
        [ $items->[7], $items->[8], $items->[9] ]
    ];
}

sub parse_value {
    my $items = shift;
    return $items->[1];
}

sub parse_name {
    my $items = shift;
    $last_name = $items->[1];
    return $items->[1];
}

sub parse_texrep {
    my $items = shift;

    return { x => $items->[1], y => $items->[2] };
}

# 12/05/2013 - added
sub parse_texoff {
    my $items = shift;

    return { x => $items->[1], y => $items->[2] };
}


sub parse_surf {
    my $items = shift;

    my $param = hex( $items->[1] );
    my $type  = $param & 0xF;
    my $types = { 0 => 'polygon', 1 => 'closedline', 2 => 'line' };

    my $shading_sides = $param >> 4;

    my $shading = $shading_sides & 0x1 ? 'smooth' : 'flat';
    my $sides   = $shading_sides & 0x2 ? 2        : 1;

    my $result = {
        type    => $types->{$type},
        shading => $shading,
        sides   => $sides,
    };

    return $result;
}

# 12/05/2013 - my 'understanding' is just take the next line, BUT if 
# given a large 'len' could maybe be several lines, but what exactly is
# the spec on this?
sub parse_data {
    my $items = shift;
    my $len = $items->[1];
    my $next = parse_line();
    # TODO: if $strict_ac3d could check the length of the following line = len
    my $result = {
        data => $next
    };
    return $result;
}

sub parse_mat {
    my $items = shift;
    my $index = $items->[1];
    # TODO: if $strict_ac3d could check the index is within range
    return $world->{MATERIAL}->[$index];

}

sub parse_loc {
    my $items = shift;
    # TODO: if $strict_ac3d could check it is 3 floats
    prt("loc $items->[1] $items->[2] $items->[3]\n") if ($show_loc_vector);
    return vec3( $items->[1], $items->[2], $items->[3] );
}

sub parse_numvert {
    my $items    = shift;
    my $numverts = $items->[1];
    my @vertices = ();
    # TODO: if $strict_ac3d could check it is 3 floats
    for ( 1 .. $numverts ) {
        my $itm = parse_line();
        push @vertices, vec3( $itm->[0], $itm->[1], $itm->[2] );
    }
    my $cnt = scalar @vertices;
    warn "parse_numvert: collected $cnt vertices ($numverts)\n" if ($show_parse_numvert);
    return \@vertices;
}

sub parse_refs {
    my $items   = shift;
    my $numrefs = $items->[1];
    # TODO: since source expects just 3 refs, perhaps normals calculation 
    # also needs to be 'fixed' if this is off!
    if ($strict_triangluation) {
        die("model is not triangulated: $last_name numrefs=$numrefs\n") if ( $numrefs != 3 );
    }
    my @indices = ();
    # TODO: if $strict_ac3d could check it is integer, in range, and 2 floats
    for ( 1 .. $numrefs ) {
        my $items = parse_line();
        push @indices,
          { index => $items->[0], u => $items->[1], v => $items->[2] };
    }
    return \@indices;
}

# SURF 0x10
# mat 1
# refs 3
# 1 0.8 1
# 0 1 1
# 5 0.8 0
sub parse_surface {
    my $items   = shift;
    my $surface = {};
    # TODO: if $strict_ac3d could check opcode order, and
    # index into vertices is valid!
    while ( my $items = parse_line() ) {
        my $opcode = $items->[0];
        if ( exists( $TOKENIZER->{$opcode} ) ) {
            $surface->{$opcode} = $TOKENIZER->{$opcode}($items);
        } else {
            my $line = join(" ",@{$items});
            prtw("WARNING: $.: parse_surface: unrecognised opcode [$opcode] in '$line'\n");
        }
        last if ( $surface->{refs} );
    }
    return $surface;
}

sub parse_numsurf {
    my $items    = shift;
    my $numsurfs = $items->[1];
    my @surfaces = ();
    for ( 1 .. $numsurfs ) {
        push @surfaces, parse_surface($items);
    }
    return \@surfaces;
}

sub parse_kids {
    my $items  = shift;
    my $amount = $items->[1];
    my @objects = ();
    warn "parse_kids: $amount...\n" if ($show_parse_kid && $amount);
    for ( 1 .. $amount ) {
        my $items = parse_line();
        push @objects, parse_object($items);
    }
    warn "parse_kids: $amount done\n" if ($show_parse_kid && $amount);
    return \@objects;
}

sub parse_object {
    my $items = shift;

    my $type   = $items->[1];
    my $object = {};
    my $error;
    $last_name = '';
    $object->{type} = $type;
    while ( my $items = parse_line() ) {
        my $opcode = $items->[0];
        if ( exists( $TOKENIZER->{$opcode} ) ) {
            $object->{$opcode} = $TOKENIZER->{$opcode}($items);
            $error = $@ if ($@);
        } else {
            $error = join(" ",@{$items});
            prtw("WARNING: $.: parse_object: unrecognised opcode [$opcode] in '$error'\n");
        }
        last if ( $opcode eq 'kids' );
    }
    if (defined $object->{name} && defined $object->{numvert}) {
        warn "parsed object: $object->{name}, type: $type, vertices: "
            . int( @{ $object->{numvert} } ) . "\n" if ($show_objects || VERB9());
    }
    return $object;
}

sub parse_material {
    my $items = shift;
    # TODO: if $strict_ac3d could check length, and verify tokens
    return {
        name  => $items->[1],
        rgb   => [ $items->[3], $items->[4], $items->[5] ],
        amb   => [ $items->[7], $items->[8], $items->[9] ],
        emis  => [ $items->[11], $items->[12], $items->[13] ],
        spec  => [ $items->[15], $items->[16], $items->[17] ],
        shi   => $items->[19],
        trans => $items->[21],
    };
}

sub parse {

    while ( my $items = parse_line() ) {
        my $command = $items->[0];
        if ( exists( $TOKENIZER->{$command} ) ) {
            push @{ $world->{$command} }, $TOKENIZER->{$command}($items);
        } else {
            my $line = join(" ",@{$items});
            prtw("WARNING: $.: parse: unrecognised command [$command] in '$line'\n");
        }
    }
    return $world;
}

# find lowest and highest vertex index
sub get_min_max_count_index {
    my $object      = shift;
    my $min_index   = 0xffffffff;
    my $max_index   = 0;
    my $index_count = 0;
    my ($ind,$surface,$ref);
    foreach $surface ( @{ $object->{numsurf} } ) {
        foreach $ref ( @{ $surface->{refs} } ) {
            $ind = $ref->{index};
            $max_index = $ind if ( $max_index < $ind );
            $min_index = $ind if ( $min_index > $ind );
            $index_count++;
        }
    }
    return ( $min_index, $max_index, $index_count );
}

# =================================================================================
###################################################################################
# my three.js replacement fucntions
###################################################################################
sub type_bitmask() {
    my $txt = <<EOF;
from : https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3
0: 0 = triangle (3 indices), 1 = quad (4 indices)
1: 0 = no face material, 1 = face material (1 index)
2: 0 = no face uvs, 1 = face uvs (1 index)
3: 0 = no face vertex uvs, 1 = face vertex uvs (3 indices or 4 indices)
4: 0 = no face normal, 1 = face normal (1 index)
5: 0 = no face vertex normals, 1 = face vertex normals (3 indices or 4 indices)
6: 0 = no face color, 1 = face color (1 index)
7: 0 = no face vertex colors, 1 = face vertex colors (3 indices or 4 indices)

   "faces": [ 

        // triangle
        // 00 00 00 00 = 0
        // 0, [vertex_index, vertex_index, vertex_index]
        0, 0,1,2,

        // quad
        // 00 00 00 01 = 1
        // 1, [vertex_index, vertex_index, vertex_index, vertex_index]
        1, 0,1,2,3,

        // triangle with material
        // 00 00 00 10 = 2
        // 2, [vertex_index, vertex_index, vertex_index],
        // [material_index]
        2, 0,1,2, 0,

        // triangle with material, vertex uvs and face normal
        // 00 10 01 10 = 38
        // 38, [vertex_index, vertex_index, vertex_index],
        // [material_index],
        // [vertex_uv, vertex_uv, vertex_uv],
        // [face_normal]
        38, 0,1,2, 0, 0,1,2, 0,

        // triangle with material, vertex uvs and vertex normals
        // 00 10 10 10 = 42
        // 42, [vertex_index, vertex_index, vertex_index],
        // [material_index],
        // [vertex_uv, vertex_uv, vertex_uv],
        // [vertex_normal, vertex_normal, vertex_normal]
        42, 0,1,2, 0, 0,1,2, 0,1,2,

        // quad with everything
        // 11 11 11 11 = 255
        // 255, [vertex_index, vertex_index, vertex_index, vertex_index],
        //  [material_index],
        //  [face_uv],
        //  [face_vertex_uv, face_vertex_uv, face_vertex_uv, face_vertex_uv],
        //  [face_normal],
        //  [face_vertex_normal, face_vertex_normal,
        //   face_vertex_normal, face_vertex_normal],
        //  [face_color]
        //  [face_vertex_color, face_vertex_color,
        //   face_vertex_color, face_vertex_color],
        255, 0,1,2,3, 0, 0, 0,1,2,3, 0, 0,1,2,3, 0, 0,1,2,3,
    ]

EOF
    return $txt;
}

sub write_json {

    my ($cnt,$i,$itm,@arr,$filename,$face_flag);

    if (length($json_file)) {
        $filename = $json_file;
    } else {
        prt("No three.js json output file name given... use -o file...\n");
        return;
    }

    open( $jsonhand, ">$filename" )
      || die("cannot write to $filename\n");

    ###warn "writing to: $filename\n";
    my $vertcnt = scalar @VERTICES;
    my $normcnt = scalar @NORMALS;
    my $surfcnt = scalar @INDPERSURF;

    my $json = "{\n";
    $json .= "\n" if ($add_line_sep);
    $json .= "\t\"metadata\" : {\n";
    $json .= "\t\t\"formatVersion\" : 3.1,\n";
    $json .= "\t\t\"generatedBy\"   : \"ac3d2threejs.pl $VERSION\",\n";
    $json .= "\t\t\"vertices\"      : $vertcnt,\n";
    $json .= "\t\t\"faces\"         : $surfcnt,\n";
    $json .= "\t\t\"normals\"       : $normcnt,\n";
    $json .= "\t\t\"colors\"        : 0,\n";
    $json .= "\t\t\"uvs\"           : [],\n";
    $json .= "\t\t\"materials\"     : 1,\n";
    $json .= "\t\t\"morphTargets\"  : 0,\n";
    $json .= "\t\t\"bones\"         : 0\n";
    $json .= "\t},\n";
    $json .= "\n" if ($add_line_sep);
    $json .= "\t\"scale\" : 1.000000,\n";
    $json .= "\n" if ($add_line_sep);

    $json .= "\t\"vertices\" : [";
    for ($i = 0; $i < $vertcnt; $i++) {
        my $vertex = $VERTICES[$i];
        $json .= $vertex->x;
        $json .= ',';
        $json .= $vertex->y;
        $json .= ',';
        $json .= $vertex->z;
        $json .= "," if (($i + 1) < $vertcnt);
    }
    $json .= "],\n";
    $json .= "\n" if ($add_line_sep);

    $json .= "\t\"morphTargets\" : [],\n";
    $json .= "\n" if ($add_line_sep);

    # Normals - positions magic = 1
    $json .= "\t\"normals\"  : [";
    for ($i = 0; $i < $normcnt; $i++) {
        my $normal = $NORMALS[$i];
        next if (!defined $normal);
        $json .= $normal->x;
        $json .= ',';
        $json .= $normal->y;
        $json .= ',';
        $json .= $normal->z;
        $json .= ',' if (($i + 1) < $normcnt);
    }
    $json .= "],\n";
    $json .= "\n" if ($add_line_sep);

    $json .= "\t\"colors\" : [],\n";
    $json .= "\n" if ($add_line_sep);
    $json .= "\t\"uvs\" : [],\n";
    $json .= "\n" if ($add_line_sep);

    $face_flag = $QuadBit;
    $face_flag += $MatBit;
    $face_flag += $VNorms if ($normcnt > 0);

    my ($tmp,$cnt2,$j,$k);
    my ($ind,$emsg);
    $json .= "\t\"faces\"  : [";
    for ($i = 0; $i < $surfcnt; $i++) {
        $tmp = $INDPERSURF[$i];
        $json .= ',' if ($i);
        $cnt2 = scalar @{$tmp};
        $json .= "$face_flag";   # 35
        $k = 0;
        for ($j = 0; $j < $cnt2; $j++) {
            $ind = ${$tmp}[$j];
            if (($ind < 0)||($ind >= $vertcnt)) {
                $emsg .= "$i:$j: V $ind/$vertcnt ";
                $ind = $vertcnt - 1;
            }
            $json .= ",$ind";
            $k++;
            last if ($k == 4);
        }
        while ($k < 4) {
            $json .= ',0';
            $k++;
        }
        $json .= ',0';
        $k = 0;
        if ($normcnt > 0) {
            for ($j = 0; $j < $cnt2; $j++) {
                $ind = ${$tmp}[$j];
                if (($ind < 0)||($ind >= $normcnt)) {
                    $emsg .= "$i:$j: N $ind/$normcnt ";
                    $ind = $normcnt - 1;
                }
                $json .= ",$ind";
                $k++;
                last if ($k == 4);
            }
            while ($k < 4) {
                $json .= ',0';
                $k++;
            }
        }
    }
    $json .= "],\n";

    prtw("WARNING: Out of range index: $emsg\n") if (length($emsg) && $show_out_of_range);

    $json .= "\n" if ($add_line_sep);

	$json .= "\t\"bones\" : [],\n";
    $json .= "\n" if ($add_line_sep);

	$json .= "\t\"skinIndices\" : [],\n";
    $json .= "\n" if ($add_line_sep);

	$json .= "\t\"skinWeights\" : [],\n";
    $json .= "\n" if ($add_line_sep);

	$json .= "\t\"animation\" : {}";

    $json .= "\n";
    $json .= "\n" if ($add_line_sep);
    $json .= "}\n";

    print $jsonhand $json;

    close($jsonhand);

    warn "Three.js json written to [$filename]\n";

}


sub parse_tree {
    my $object = shift;

    my $tag    = '';
    my $params = {};
    
    my $msg = "parse_tree: ";
    if ($object->{name}) {
        $msg .= "$object->{name} ";
    } else {
        $msg .= "NN ";
    }

    if ( $object->{loc} ) {
        $params->{tx} = $object->{loc}->x;
        $params->{ty} = $object->{loc}->y;
        $params->{tz} = $object->{loc}->z;
    }

    if ( $object->{type} eq 'world' ) {
        $tag = 'Group';
        $params->{name} = 'world';
        $msg .= "world ";
    }
    elsif ( $object->{type} eq 'group' ) {
        $tag                = 'Model';
        $params->{name}     = $object->{name};
        $params->{geometry} = "$scene_basename/$scene_basename.geo";
        $object->{in_model} = 1;
        $msg .= "group ";
    }
    elsif ( $object->{type} eq 'poly' ) {
        $msg .= "poly ";
        if ( !$object->{in_model} )
        {    # whoops not in a model yet, create one first
            my %wrapper = %$object;
            $wrapper{kids} = [$object];
            $wrapper{type} = 'group';
            ### delete $object->{loc};    # set location for model in this case
            ### prt("$msg not in model\n") if (VERB9());
            parse_tree( \%wrapper );
            return;
        }
        else {

            $tag = 'Mesh';

            calc_vertex_normals($object);

            ### warn Dumper($object);
            ###my $material = basename( $object->{texture} ) || 'standard';
            ##$params->{material} = "$scene_basename/$material.material.xml";
            $params->{name}     = $object->{name};

            my ( $min_index, $max_index, $index_count ) =
              get_min_max_count_index($object);

            $params->{vertRStart} = $min_index + $index_offset;
            $params->{vertREnd}   = $max_index + $index_offset;

            $params->{batchCount} = $index_count;
            $params->{batchStart} = $batch_offset;

            if (defined $object->{numvert}) {
                push @VERTICES,   @{ $object->{numvert} };
            }
            if (defined $object->{normals}) {
                push @NORMALS,    @{ $object->{normals} };
            }
            ##push @TANGENTS,   @{ $object->{tangents} };
            ##push @BITANGENTS, @{ $object->{bitangents} };
            ##push @TCOORDS0,   @{ $object->{tcoords} };

            # @INDPERSURF
            if ((defined $object->{numsurf})&&(defined $object->{numvert})) {
                my @inds = ();
                foreach my $surface ( @{ $object->{numsurf} } ) {
                    @inds = ();
                    foreach my $ref ( @{ $surface->{refs} } ) {
                        push @INDICES, $ref->{index} + $index_offset;
                        push @inds, $ref->{index} + $index_offset;
                    }
                    push(@INDPERSURF, [@inds]); # keep on a per face basis
                }
                $index_offset += int( @{ $object->{numvert} } );
                $batch_offset += $index_count;
            }

        }
    }

    ###my $attributes = '';
    ###foreach my $key ( sort keys(%$params) ) {
    ###    $attributes .= qq~ $key="$params->{$key}"~;
    ##}
    my $kcnt = 0;
    foreach my $kid ( @{ $object->{kids} } ) {
        $kid->{in_model} = $object->{in_model};
        parse_tree($kid);
        $kcnt++;
    }

    #### prt("$msg kid $kcnt\n") if ($kcnt);

    delete $object->{kids};
}


sub write_object {
    my $tree = shift;
    parse_tree( $tree->{OBJECT}->[0] );
}

# =================================================================================
###################################################################################

sub uv_sub {
    my $uv1 = shift;
    my $uv2 = shift;

    return {
        u => $uv1->{u} - $uv2->{u},
        v => $uv1->{v} - $uv2->{v}
    };
}

# find vertices with same coordinates, this is for normal smoothing, return of list of matching indices
sub find_same_vertices {
    my $object      = shift;
    my $indices_map = shift;
    my $index       = shift;
    my @indices     = ();

    if (defined $object->{numvert}->[$index] && defined $indices_map) {
        my $vertex  = $object->{numvert}->[$index];
        my $vstring = $vertex->as_string;
        if (defined $indices_map->{ $vstring }) {
            @indices = @{ $indices_map->{ $vstring } };
            push( @indices, $index ) if ( !grep { $_ == $index } @indices );
        }
    }
    return @indices;
}

# create map of indices with same vertices
sub make_indices_map {
    my $object = shift;

    my %indices_map;

    foreach my $surface ( @{ $object->{numsurf} } ) {

        # only average out smooth vertex normals
        next if ( $surface->{SURF}->{shading} ne 'smooth' );
        foreach my $ref ( @{ $surface->{refs} } ) {

            my $v = $object->{numvert}->[ $ref->{index} ];
            push @{ $indices_map{ $v->as_string } }, $ref->{index};
        }
    }

    #    warn Dumper(\%indices_map);

    return \%indices_map;
}

sub calc_vertex_normals {
    my $object = shift;
    if (!defined $object->{numvert}) {
        prt("calc_vertex_normals: object->{numvert} NOT defined\n");
        return;
    }
    if (!defined $object->{numsurf}) {
        prt("calc_vertex_normals: object->{numsurf} NOT defined\n");
        return;
    }

    my $vcnt = scalar @{ $object->{numvert} };
    my $scnt = scalar @{ $object->{numsurf} };
    my @normals = ();
    if (!$add_normals) {
        warn "no normals: $object->{name} v=$vcnt, s=$scnt\n" if ($show_calc_norms);
        $object->{normals} = \@normals;
        return;
    }

    warn "calculating normals: $object->{name} v=$vcnt, s=$scnt\n" if ($show_calc_norms);
    my ($i,$surface,$refs,$index0,$index1,$index2);
    my ($v0,$v1,$v2);
    my ($d0,$d1,$v);
    foreach my $surface ( @{ $object->{numsurf} } ) {
        $refs = scalar @{ $surface->{refs} };
        if ($refs >= 3) {
            for ($i = 0; $i < $refs; $i++) {
                $index0 = $surface->{refs}->[$i]->{index};
                # wrapping
                if (($i + 1) >= $refs) {
                    $index1 = $surface->{refs}->[0]->{index};
                    $index2 = $surface->{refs}->[1]->{index};
                } elsif (($i + 2) >= $refs) {
                    $index1 = $surface->{refs}->[$i+1]->{index};
                    $index2 = $surface->{refs}->[0]->{index};
                } else {
                    $index1 = $surface->{refs}->[$i+1]->{index};
                    $index2 = $surface->{refs}->[$i+2]->{index};
                }
                # get the three vertices
                $v0 = $object->{numvert}->[$index0];
                $v1 = $object->{numvert}->[$index1];
                $v2 = $object->{numvert}->[$index2];
                # get difference
                $d0 = $v1 - $v0;
                $d1 = $v2 - $v0;
                # get normal
                $v  = ( $d0 x $d1 )->normalized;
                $normals[$index0] = $v; # store at index
            }
        }
    }

    if ($vcnt) {
        my ($vert,$x,$y,$z,$dx,$dy,$dz);
        $vert = ${$object->{numvert}}[0];
        my $minx = $vert->x;
        my $maxx = $vert->x;
        my $miny = $vert->y;
        my $maxy = $vert->y;
        my $minz = $vert->z;
        my $maxz = $vert->z;
        if ($obj_count == 0) {
            $g_minx = $vert->x;
            $g_maxx = $vert->x;
            $g_miny = $vert->y;
            $g_maxy = $vert->y;
            $g_minz = $vert->z;
            $g_maxz = $vert->z;
        }
        $obj_count++;
        $vert_count += $vcnt;
        for ($v = 1; $v < $vcnt; $v++) {
            $vert = ${$object->{numvert}}[$v];
            $x = $vert->x;
            $y = $vert->y;
            $z = $vert->z;
            ###prt("$x,$y,$z\n");
            $minx = $x if ($x < $minx);
            $maxx = $x if ($x > $maxx);
            $miny = $y if ($y < $miny);
            $maxy = $y if ($y > $maxy);
            $minz = $z if ($z < $minz);
            $maxz = $z if ($z > $maxz);
            $g_minx = $x if ($x < $g_minx);
            $g_maxx = $x if ($x > $g_maxx);
            $g_miny = $y if ($y < $g_miny);
            $g_maxy = $y if ($y > $g_maxy);
            $g_minz = $z if ($z < $g_minz);
            $g_maxz = $z if ($z > $g_maxz);
        }
        $dx = int($maxx - $minx);
        $dy = int($maxy - $miny);
        $dz = int($maxz - $minz);
        my $locx = 0;
        my $locy = 0;
        my $locz = 0;
        if ( $object->{loc} ) {
            $locx = $object->{loc}->x;
            $locy = $object->{loc}->y;
            $locz = $object->{loc}->z;
        }
        my $rotx = 0;
        my $roty = 0;
        my $rotz = 0;
        if ( $object->{rot} ) {
            $rotx = $object->{rot}->x;
            $roty = $object->{rot}->y;
            $rotz = $object->{rot}->z;
        }

        prt("$object->{name} diff: $dx $dy $dz loc $locx $locy $locz rot $rotx $roty $rotz\n")
            if ($show_objects || VERB9());
        ###prt("min: $minx $miny $minz max: $maxx $maxy $maxz\n");
    } else {
        prt("$object->{name} no vertices\n");
    }

    $object->{normals} = \@normals;
}

sub calc_vertex_normals_OLD {
    my $object = shift;
    my $vcnt = scalar @{ $object->{numvert} };
    my $scnt = scalar @{ $object->{numsurf} };

    warn "calculating normals: $object->{name} v=$vcnt, s=$scnt\n" if ($show_calc_norms);

    my @normals;
    my @tangents;
    my @bitangents;

    my $indices_map = make_indices_map($object);

    foreach my $surface ( @{ $object->{numsurf} } ) {

        my $index0 = $surface->{refs}->[0]->{index};
        my $index1 = $surface->{refs}->[1]->{index};
        my $index2 = $surface->{refs}->[2]->{index};

        my $v0 = $object->{numvert}->[$index0];
        my $v1 = $object->{numvert}->[$index1];
        my $v2 = $object->{numvert}->[$index2];

        my $d0 = $v1 - $v0;
        my $d1 = $v2 - $v0;
        my $v  = ( $d0 x $d1 )->normalized;

        #
        #  Calculate the normal for this face and add it
        #  to the running sum of normals for each of the
        #  three vertices involved
        #

        # and now tangents and bitangents
        my $Edge0uv = uv_sub( $surface->{refs}->[1], $surface->{refs}->[0] );
        my $Edge1uv = uv_sub( $surface->{refs}->[2], $surface->{refs}->[0] );

        my $cp = $Edge0uv->{u} * $Edge1uv->{v} - $Edge1uv->{u} * $Edge0uv->{v};
        my $r  = 0;
        if ( $cp != 0 ) {
            $r = 1.0 / $cp;
        }

        my $tangent   = ( $d0 * $Edge1uv->{v} - $d1 * $Edge0uv->{v} ) * $r;
        my $bitangent = ( $d1 * $Edge0uv->{u} - $d0 * $Edge1uv->{u} ) * $r;

        my @indices;
        push @indices, find_same_vertices( $object, $indices_map, $index0 );
        push @indices, find_same_vertices( $object, $indices_map, $index1 );
        push @indices, find_same_vertices( $object, $indices_map, $index2 );
        foreach my $index (@indices) {
            $normals[$index]    ||= vec3();
            $tangents[$index]   ||= vec3();
            $bitangents[$index] ||= vec3();

            $normals[$index]    += $v;
            $tangents[$index]   += $tangent;
            $bitangents[$index] += $bitangent;
        }

    }

    #  Normalize and fixup the vertex normal vectors, tangents en bitangents
    my $numVerts = int( @{ $object->{numvert} } );
    for ( my $j = 0 ; $j < $numVerts ; $j++ ) {
        next if (! defined $normals[$j]);
        my $n = $normals[$j]->normalized;
        my $t = $tangents[$j];

        # orthogonalize
        $tangents[$j] = ( $t - $n * ( $n * $t ) )->normalized;
        $normals[$j] = $n;

        if ( ( $n x $t ) * $bitangents[$j] < 0 ) {
            $bitangents[$j] = ( ( $n * -1 ) x $t )->normalized;
        }
        else {
            $bitangents[$j] = ( $n x $t )->normalized;
        }
    }

    $object->{normals}    = \@normals;
    $object->{tangents}   = \@tangents;
    $object->{bitangents} = \@bitangents;
}

sub round {
    my $number = shift;
    return int( $number + .5 * ( $number <=> 0 ) );
}

sub show_bbox() {
    my $dx = int($g_maxx - $g_minx);
    my $dy = int($g_maxy - $g_miny);
    my $dz = int($g_maxz - $g_minz);
    prt("objects: $obj_count diff: x=$dx y=$dy z=$dz total verts: $vert_count\n") if ($obj_count > 1);
}


# TODO: Add more command line options
sub main {
    my @av = @_;

    parse_command(@av);

    open( $ac3dfile, "<$source" ) || die "cannot open [$source]\n";

    $scene_basename = basename($source);
    my $header = parse_line();
    parse_header($header);
    my $tree = parse();

    write_object($tree);

    write_json();

    show_bbox();

    $warn_count = scalar @warnings;
    if ($warn_count) {
        foreach my $w (@warnings) {
            prt($w);
        }
    }
    warn "Done file [$source], lines $., exit $warn_count\n";
    close($ac3dfile);
}

sub give_help() {
    warn "AC3DParse.pl $VERSION - Parse an AC3D .ac file, and report objects found.\n";
    warn "usage: $pgmname [options] file.ac\n";
    warn "options:\n";
    warn " --help (-h or -?) = This help and exit.\n";
    warn " --norm 0|1   (-n) = Disable/Enable normals generation. (def=$add_normals)\n";
    prt( " --verb[n]    (-v) = Bump [or set] verbosity. def=$verbosity\n");
    warn " --out file   (-o) = Give three.js json output file name.\n";
    die  "enjoy!\n";
}

sub need_arg {
    my ($arg,@av) = @_;
    die "ERROR: [$arg] must have a following argument!\n" if (!@av);
}

sub get_bool($) {
    my $txt = shift;
    return 1 if ($txt eq '1');
    return 0 if ($txt eq '0');
    return 1 if ($txt =~ /on/i);
    return 0 if ($txt =~ /off/i);
    return 1 if ($txt =~ /yes/i);
    return 0 if ($txt =~ /no/i);
    die "Unable to parse bool 1|0 on|off yes|no from $txt\n";
}

sub parse_command() {
    my (@av) = @_;
    my ($arg,$sarg);
    #$arg = scalar @av;
    #warn "Parsing $arg commands...\n";
    while (@av) {
        $arg = $av[0];
        #warn "arg [$arg]\n";
        if ($arg =~ /^-/) {
            $sarg = substr($arg,1);
            $sarg = substr($sarg,1) while ($sarg =~ /^-/);
            if (($sarg =~ /^h/)||($sarg =~ /^\?/)) {
                give_help();
            } elsif ($sarg =~ /^n/) {
                need_arg(@av);
                shift @av;
                $sarg = $av[0];
                $add_normals = get_bool($sarg);
            } elsif ($sarg =~ /^o/) {
                need_arg(@av);
                shift @av;
                $json_file = $av[0];
            } elsif ($sarg =~ /^v/) {
                if ($sarg =~ /^v.*(\d+)$/) {
                    $verbosity = $1;
                } else {
                    while ($sarg =~ /^v/) {
                        $verbosity++;
                        $sarg = substr($sarg,1);
                    }
                }
                prt("Verbosity = $verbosity\n") if (VERB1());
            } else {
                die "Unknown arg $arg. Use -h for help\n";
            }

        } else {
            $source = $arg;
            warn "Set source to [$source]\n";
        }
        shift @av;
    }
    if ($debug_on) {
        prtw("WARNING: DEBUG is ON\n");
        if (length($source) == 0) {
            $source = $def_file;
        }
    }
    if (length($source) == 0) {
        die "Need to give AC3D file to parse. Use -h for help\n";
    }

}

# eof - ac3dparse.pl
