use strict;
use warnings;
use Data::Dumper;
use File::Find;
use XML::Simple;
use JSON;

use Entity;

use constant CHECK_MAPS_XML => 0;
use constant ROOT_ACTORS => 1;

my @files;
my @roots;
my @deps;

my $vfsroot = '../../../binaries/data/mods';

sub vfs_to_physical
{
    my ($vfspath) = @_;
    my $fn = "$vfsroot/public/$vfspath";
    if (not -e $fn)
    {
        $fn = "$vfsroot/mod/$vfspath";
    }
    return $fn;
}

sub vfs_to_relative_to_mods
{
    my ($vfspath) = @_;
    my $fn = "public/$vfspath";
    return $fn;
}

sub find_files
{
    my ($vfspath, $extn) = @_;
    my @files;
    my $find_process = sub {
        return $File::Find::prune = 1 if $_ eq '.svn';
        my $n = $File::Find::name;
        return if /~$/;
        return unless -f $_;
        return unless /\.($extn)$/;
        $n =~ s~\Q$vfsroot\E/(public|mod)/~~;
        push @files, $n;
    };
    find({ wanted => $find_process }, "$vfsroot/public/$vfspath");
    find({ wanted => $find_process }, "$vfsroot/mod/$vfspath") if -d "$vfsroot/mod/$vfspath";

    return @files;
}

sub parse_json_file
{
    my ($vfspath) = @_;
    open my $fh, vfs_to_physical($vfspath) or die "Failed to open '$vfspath': $!";
    # decode_json expects a UTF-8 string and doesn't handle BOMs, so we strip those
    # (see http://trac.wildfiregames.com/ticket/1556)
    return decode_json(do { local $/; my $file = <$fh>; $file =~ s/^\xEF\xBB\xBF//; $file });
}

sub add_entities
{
    print "Loading entities...\n";

    my @entfiles = find_files('simulation/templates', 'xml');
    s~^simulation/templates/(.*)\.xml$~$1~ for @entfiles;

    for my $f (sort @entfiles)
    {
        my $path = "simulation/templates/$f.xml";
        push @files, $path;
        my $ent = Entity::load_inherited($f);

        push @deps, [ $path, "simulation/templates/" . $ent->{Entity}{'@parent'}{' content'} . ".xml" ] if $ent->{Entity}{'@parent'};

        if ($f !~ /^template_/)
        {
            push @roots, $path;
            if ($ent->{Entity}{VisualActor})
            {
                push @deps, [ $path, "art/actors/" . $ent->{Entity}{VisualActor}{Actor}{' content'} ] if $ent->{Entity}{VisualActor}{Actor};
                push @deps, [ $path, "art/actors/" . $ent->{Entity}{VisualActor}{FoundationActor}{' content'} ] if $ent->{Entity}{VisualActor}{FoundationActor};
            }

            if ($ent->{Entity}{Sound})
            {
                push @deps, [ $path, "audio/" . $_->{' content'} ] for grep ref($_), values %{$ent->{Entity}{Sound}{SoundGroups}};
            }

            if ($ent->{Entity}{Identity})
            {
                push @deps, [ $path, "art/textures/ui/session/portraits/" . $ent->{Entity}{Identity}{Icon}{' content'} ] if $ent->{Entity}{Identity}{Icon};
            }
        }
    }
}

sub add_actors
{
    print "Loading actors...\n";

    my @actorfiles = find_files('art/actors', 'xml');
    for my $f (sort @actorfiles)
    {
        push @files, $f;

        push @roots, $f if ROOT_ACTORS;

        my $actor = XMLin(vfs_to_physical($f), ForceArray => [qw(group variant texture prop animation)], KeyAttr => []) or die "Failed to parse '$f': $!";

        for my $group (@{$actor->{group}})
        {
            for my $variant (@{$group->{variant}})
            {
                push @deps, [ $f, "art/meshes/$variant->{mesh}" ] if $variant->{mesh};
                push @deps, [ $f, "art/particles/$variant->{particles}{file}" ] if $variant->{particles}{file};
                for my $tex (@{$variant->{textures}{texture}})
                {
                    push @deps, [ $f, "art/textures/skins/$tex->{file}" ] if $tex->{file};
                }
                for my $prop (@{$variant->{props}{prop}})
                {
                    push @deps, [ $f, "art/actors/$prop->{actor}" ] if $prop->{actor};
                }
                for my $anim (@{$variant->{animations}{animation}})
                {
                    push @deps, [ $f, "art/animation/$anim->{file}" ] if $anim->{file};
                }
            }
        }

        push @deps, [ $f, "art/materials/$actor->{material}" ] if $actor->{material};
    }
}

sub add_art
{
    print "Loading art files...\n";
    push @files, find_files('art/textures/particles', 'dds|png|jpg|tga');
    push @files, find_files('art/textures/terrain', 'dds|png|jpg|tga');
    push @files, find_files('art/textures/skins', 'dds|png|jpg|tga');
    push @files, find_files('art/meshes', 'pmd|dae');
    push @files, find_files('art/animation', 'psa|dae');
}

sub add_materials
{
    print "Loading materials...\n";
    my @materialfiles = find_files('art/materials', 'xml');
    for my $f (sort @materialfiles)
    {
        push @files, $f;

        my $material = XMLin(vfs_to_physical($f), ForceArray => [qw(alternative)], KeyAttr => []);
        for my $alternative (@{$material->{alternative}})
        {
            push @deps, [ $f, "art/materials/$alternative->{material}" ] if $alternative->{material};
        }
    }
}

sub add_particles
{
    print "Loading particles...\n";
    my @particlefiles = find_files('art/particles', 'xml');
    for my $f (sort @particlefiles)
    {
        push @files, $f;

        my $particle = XMLin(vfs_to_physical($f));
        push @deps, [ $f, "$particle->{texture}" ] if $particle->{texture};
    }
}

sub add_maps_xml
{
    print "Loading maps XML...\n";
    my @mapfiles = find_files('maps/scenarios', 'xml');
    push @mapfiles, find_files('maps/skirmishes', 'xml');
    for my $f (sort @mapfiles)
    {
        print "  $f\n";

        push @files, $f;

        push @roots, $f;

        my $map = XMLin(vfs_to_physical($f), ForceArray => [qw(Entity)], KeyAttr => []) or die "Failed to parse '$f': $!";

        my %used;
        for my $entity (@{$map->{Entities}{Entity}})
        {
            $used{$entity->{Template}} = 1;
        }

        for my $template (keys %used)
        {
            if ($template =~ /^actor\|(.*)$/)
            {
                # Handle special 'actor|' case
                push @deps, [ $f, "art/actors/$1" ];
            }
            else
            {
                if ($template =~ /^resource\|(.*)$/)
                {
                    # Handle special 'resource|' case
                    $template = $1;
                }
                push @deps, [ $f, "simulation/templates/$template.xml" ];
            }
        }

        # Map previews
        my $settings = decode_json($map->{ScriptSettings});
        push @deps, [ $f, "art/textures/ui/session/icons/mappreview/" . $settings->{Preview} ] if $settings->{Preview};
    }
}

sub add_maps_pmp
{
    print "Loading maps PMP...\n";

    # Need to generate terrain texture filename=>path lookup first
    my %terrains;
    for my $f (find_files('art/terrains', 'xml'))
    {
        $f =~ /([^\/]+)\.xml/ or die;

        # ignore terrains.xml
        if ($f !~ /terrains.xml$/)
        {
            warn "Duplicate terrain name '$1' (from '$terrains{$1}' and '$f')\n" if $terrains{$1};
            $terrains{$1} = $f;
        }
    }

    my @mapfiles = find_files('maps/scenarios', 'pmp');
    push @mapfiles, find_files('maps/skirmishes', 'pmp');
    for my $f (sort @mapfiles)
    {
        push @files, $f;

        push @roots, $f;

        open my $fh, vfs_to_physical($f) or die "Failed to open '$f': $!";
        binmode $fh;

        my $buf;

        read $fh, $buf, 4;
        die "Invalid PMP file ($buf)" unless $buf eq "PSMP";

        read $fh, $buf, 4;
        my $version = unpack 'V', $buf;
        die "Invalid PMP file ($version)" unless $version == 5;

        read $fh, $buf, 4;
        my $datasize = unpack 'V', $buf;

        read $fh, $buf, 4;
        my $mapsize = unpack 'V', $buf;

        seek $fh, 2 * ($mapsize*16+1)*($mapsize*16+1), 1; # heightmap

        read $fh, $buf, 4;
        my $numtexs = unpack 'V', $buf;

        for (0..$numtexs-1)
        {
            read $fh, $buf, 4;
            my $len = unpack 'V', $buf;
            my $str;
            read $fh, $str, $len;

            push @deps, [ $f, $terrains{$str} || "art/terrains/(unknown)/$str" ];
        }

        # ignore patches data
    }
}

sub add_soundgroups
{
    print "Loading sound groups...\n";
    my @soundfiles = find_files('audio', 'xml');
    for my $f (sort @soundfiles)
    {
        push @files, $f;

        my $sound = XMLin(vfs_to_physical($f), ForceArray => [qw(Sound)], KeyAttr => []) or die "Failed to parse '$f': $!";

        my $path = $sound->{Path};
        $path =~ s/\/$//; # strip optional trailing slash

        for (@{$sound->{Sound}})
        {
            push @deps, [$f, "$path/$_" ];
        }

        push @deps, [$f, "$path/$sound->{Replacement}" ] if $sound->{Replacement} and not ref $sound->{Replacement};
    }
}

sub add_audio
{
    print "Loading audio files...\n";
    push @files, find_files('audio', 'ogg');
}

sub add_gui_xml
{
    print "Loading GUI XML...\n";
    my @guifiles = find_files('gui', 'xml');
    for my $f (sort @guifiles)
    {
        push @files, $f;

        if ($f =~ /^gui\/page_/)
        {
            push @roots, $f;
            my $xml = XMLin(vfs_to_physical($f), ForceArray => [qw(include)], KeyAttr => []) or die "Failed to parse '$f': $!";

            push @deps, [ $f, "gui/$_" ] for @{$xml->{include}};
        }
        else
        {
            my $xml = XMLin(vfs_to_physical($f), ForceArray => [qw(object script action sprite image)], KeyAttr => [], KeepRoot => 1) or die "Failed to parse '$f': $!";
            my $name = (keys %$xml)[0];
            if ($name eq 'objects' or $name eq 'object')
            {
                push @deps, [ $f, $_->{file} ] for grep { ref $_ and $_->{file} } @{$xml->{objects}{script}};
                my $add_objects;
                $add_objects = sub
                {
                    my ($parent) = @_;
                    for my $obj (@{$parent->{object}})
                    {
                        # TODO: look at sprites, styles, etc
                        $add_objects->($obj);
                    }
                };
                $add_objects->($xml->{objects});
            }
            elsif ($name eq 'setup')
            {
                # TODO: look at sprites, styles, etc
            }
            elsif ($name eq 'styles')
            {
                # TODO: look at sprites, styles, etc
            }
            elsif ($name eq 'sprites')
            {
                for my $sprite (@{$xml->{sprites}{sprite}})
                {
                    for my $image (@{$sprite->{image}})
                    {
                        push @deps, [ $f, "art/textures/ui/$image->{texture}" ] if $image->{texture};
                    }
                }
            }
            else
            {
                print "Unexpected GUI XML root element '$name':\n" . Dumper $xml;
                exit;
            }
        }
    }
}

sub add_gui_data
{
    print "Loading GUI data...\n";
    push @files, find_files('gui', 'js');
    push @files, find_files('art/textures/ui', 'dds|png|jpg|tga');
}

sub add_civs
{
    print "Loading civs...\n";

    my @civfiles = find_files('civs', 'json');
    for my $f (sort @civfiles)
    {
        push @files, $f;

        push @roots, $f;

        my $civ = parse_json_file($f);

        push @deps, [ $f, "art/textures/ui/" . $civ->{Emblem} ];

        push @deps, [ $f, "audio/music/" . $_->{File} ] for @{$civ->{Music}};
    }
}

sub add_rms
{
    print "Loading random maps...\n";

    push @files, find_files('maps/random', 'js');
    my @rmsdefs = find_files('maps/random', 'json');

    for my $f (sort @rmsdefs)
    {
        push @files, $f;

        push @roots, $f;

        my $rms = parse_json_file($f);

        push @deps, [ $f, "maps/random/" . $rms->{settings}{Script} ];

        # Map previews
        push @deps, [ $f, "art/textures/ui/session/icons/mappreview/" . $rms->{settings}{Preview} ] if $rms->{settings}{Preview};
    }
}

sub add_techs
{
    print "Loading techs...\n";

    my @techfiles = find_files('simulation/data/technologies', 'json');
    for my $f (sort @techfiles)
    {
        push @files, $f;
        push @roots, $f;

        my $tech = parse_json_file($f);

        push @deps, [ $f, "art/textures/ui/session/portraits/technologies/" . $tech->{icon} ] if $tech->{icon};
        push @deps, [ $f, "simulation/data/technologies/" . $tech->{supersedes} . ".json" ] if $tech->{supersedes};
    }
}

sub add_terrains
{
    print "Loading terrains...\n";

    my @terrains = find_files('art/terrains', 'xml');
    for my $f (sort @terrains)
    {
        # ignore terrains.xml
        if ($f !~ /terrains.xml$/)
        {
            push @files, $f;
            
            my $terrain = XMLin(vfs_to_physical($f), ForceArray => [qw(texture)], KeyAttr => []) or die "Failed to parse '$f': $!";

            for my $texture (@{$terrain->{textures}{texture}})
            {
                push @deps, [ $f, "art/textures/terrain/$texture->{file}" ] if $texture->{file};
            }
            push @deps, [ $f, "art/materials/$terrain->{material}" ] if $terrain->{material};
        }
    }
}


sub check_deps
{
    my %files;
    @files{@files} = ();

    my %lcfiles;
    @lcfiles{map lc($_), @files} = @files;

    my %revdeps;
    for my $d (@deps)
    {
        push @{$revdeps{$d->[1]}}, $d->[0];
    }

    for my $f (sort keys %revdeps)
    {
        next if exists $files{$f};
        warn "Missing file '$f' referenced by: " . (join ', ', map "'$_'", map vfs_to_relative_to_mods($_), sort @{$revdeps{$f}}) . "\n";

        if (exists $lcfiles{lc $f})
        {
            warn "### Case-insensitive match (found '$lcfiles{lc $f}')\n";
        }
    }
}

sub check_unused
{
    my %reachable;
    @reachable{@roots} = ();

    my %deps;
    for my $d (@deps)
    {
        push @{$deps{$d->[0]}}, $d->[1];
    }

    while (1)
    {
        my @newreachable;
        for my $r (keys %reachable)
        {
            push @newreachable, grep { not exists $reachable{$_} } @{$deps{$r}};
        }
        last if @newreachable == 0;
        @reachable{@newreachable} = ();
    }

    for my $f (sort @files)
    {
        next if exists $reachable{$f};
        warn "Unused file '" . vfs_to_relative_to_mods($f) . "'\n";
    }
}


add_maps_xml() if CHECK_MAPS_XML;

add_maps_pmp();

add_entities();

add_actors();

add_art();

add_materials();

add_particles();

add_soundgroups();
add_audio();

add_gui_xml();
add_gui_data();

add_civs();

add_rms();

add_techs();

add_terrains();

# TODO: add non-skin textures, and all the references to them

print "\n";
check_deps();
print "\n";
check_unused();
