package view;

use strict;
use warnings;
require 'doxy-link.pm';
use Dotiac::DTL qw/Template/;
use Dotiac::DTL::Addon::markup;
use ASF::Util qw/read_text_file shuffle/;
use File::Temp qw/tempfile/;
use LWP::Simple;
use File::Spec;
use Cwd 'realpath';
use Carp;

# The top directory is the outermost directory that has an order file.
use constant ORDER_FILE => 'order.txt';
# External variable file
use constant VAR_FILE => 'vars.txt';
# The stem that marks an index file
use constant INDEX_STEM => 'index';
# Name of the generated nav tree file
use constant NAV_TREE_FILE => 'tree.nav';

# Root directory for all content.
# This should be the relative path from the source control base directory
# to the documentation root directory (the argument to --sourcebase with the
# source control directory clipped off the front.)
use constant SRC_DIR => 'content';
# Currently required suffixes for content files.
our %SUFFIXES = ( mdtext => 1 );
# Page base if set
our $PAGE_BASE;
# Target directory if set
our $TARGET_BASE;
# Language to use when we can't find what we want.
use constant DEFAULT_LANG => 'en';

# Split a content file in to its parts - ( stem, lang, format )
sub _SplitContentName {
    my ($name) = @_;
    my @zret = ('', '', ''); # default to empty strings.
    $_[0] =~ m!([^/]+)$!; # strip any leading directories.

    confess 'Invalid content file path "', $_[0], "\"\n" unless $name;
    @zret = ($1, $2, $3) if $name =~ m!^(.+)[.]([^.]+)[.]([^.]+)$!;
    return @zret;
}

# This is basically File::Spec::catdir except it makes a relative path
# of just 'dir' if the base path is empty.
sub _append_to_path {
    my ($path, $dir) = @_;
    return $path ? File::Spec->catdir($path, $dir) : $dir;
}

package view::Source;
# A source / content file. This is a terminal in the documentation tree.
# It has a map of language tags -> original file.

# _LANGMAP -> Hash from language tag to source file data.
#   _FILE -> Local file name.
#   _LINK -> File name for HTML links.
#   _VARS -> Internal file variables.
#  
# _STEM -> The stem for this source.
# _PATH -> Path of the directory containing this source.
# _VARS -> External variables for stem.

sub new {
    my ($self, $path, $stem) = @_;
    $self = bless {}, $self if not ref $self;
    $self->_init;
    $self->{_PATH} = $path;
    $self->{_STEM} = $stem;
    return $self;
}

sub _init {
    my $self = shift;
    $self->{_LANGMAP} = {};
    $self->{_VARS} = {};
    return $self;
}

# accessors
sub stem { return $_[0]->{_STEM}; }
sub path { return $_[0]->{_PATH}; }
# This is a convenience so we don't have to check for being a node.
sub srcCount { return 0; }
sub isTop { return 0; }

sub parent {
    my ($self) = @_;
    return view::Node::GetLocation($self->{_PATH});
}

sub top {
    my ($node) = @_;
    while (not $node->isTop) {
        $node = $node->parent;
    }
    return $node;
}

sub vars {
    my ($self, $vars) = @_;
    my $zret = $self->{_VARS};
    $self->{_VARS} = $vars if scalar @_ > 1;
    return $zret;
}

sub setFileForLang {
    my ($self, $file, $lang) = @_;
    my $link = join('.', (view::_SplitContentName($file))[0,1], 'html');
    $self->{_LANGMAP}{$lang} = {
        _FILE => $file,
        _LINK => $link
    };
}

# Access the language specific data
sub langToData {
    my ($self, $lang) = @_;
    my $zret = $self->{_LANGMAP}{$lang} || $self->{_LANGMAP}{view::DEFAULT_LANG()};
    die "Stem '", $self->stem, "' in '", $self->{_PATH}, "' does not have language '$lang' or the default '", view::DEFAULT_LANG(), "'.\n" unless $zret;
    # Fault in the file at this point.
    if (not $zret->{_VARS}) {
        my %vars = %{$self->vars}; # copy external variables over.
        my $var_path = File::Spec->catfile($self->path, $zret->{_FILE});
        view::read_text_file $var_path, \%vars; # add file internal vars.
        $zret->{_VARS} = \%vars;
    }
    return $zret;
}

# Get/Set the file for lang. This should be the local file name, not a path.
sub langToFile {
    my ($self, $lang, $file) = @_;
    return $self->langToData($lang)->{_FILE};
}

# Get the link name for @a lang.
sub langToLink {
    my ($self, $lang) = @_;
    return $self->langToData($lang)->{_LINK};
}

# Get an absolute (for the web server) path for a link to this source.
sub absLinkPath {
    my ($self) = @_;
    my $path = $self->path;
    $path = substr($path, length(view::SRC_DIR()), length($path));
    return $path;
}

# Get the variables (including content) for a language.
sub langToVars {
    return $_[0]->langToData($_[1])->{_VARS};
}

# Set the lang map / data directly.
# Used by stem loading logic.
sub copyLangMap {
    my ($self, $that) = @_;
    $self->{_LANGMAP} = $that->{_LANGMAP};
    return $self;
}

sub langList {
    my ($self) = @_;
    return keys %{$self->{_LANGMAP}};
}

# Compute a relative path from this source to another.
sub pathTo {
    my ($self, $dest) = @_;
    my @spath = File::Spec->splitdir($self->path);
    my @dpath = File::Spec->splitdir($dest->path);
    my $rpath = '';

    # strip off matching ancestors from the top.    
    while (scalar @spath and scalar @dpath and $spath[0] eq $dpath[0]) {
        shift @spath;
        shift @dpath;
    }
    # Now work up the source path.
    while (scalar @spath) {
        $rpath = view::_append_to_path($rpath, '..');
        pop @spath;
    }
    # Then back down the destination.
    while (scalar @dpath) {
        $rpath = view::_append_to_path($rpath, shift @dpath);
    }
    return $rpath;
}

package view::Node;
our @ISA = ( 'view::Source' );
# Boolean constants
use constant TRUE => 1;
use constant FALSE => 0;
use Carp;
use Storable;

# Map from directory paths to Nodes.
our %Locations;

# A non-terminal node in the documentation hierarchy. It must have some content
# stored in either the 'index' file or a file with the same stem as the
# containing directory. These are stored in a global hash of directory path
# to Node instance.

# _ORDER -> stem ordering.
# _TOP_P -> is this a top level node?
# _PATH -> Path to this directory, which is the containing directory for
#          the node content (which is always a file in this directory).

sub _init {
    my $self = shift;
    $self->{_ORDER} = [];
    $self->{_TOP_P} = 0;
    $self->SUPER::_init;
    return $self;
}

sub isTop {
    my ($self, $flag) = @_;
    my $zret = $self->{_TOP_P};
    $self->{_TOP_P} = $flag if scalar @_ > 1;
    return $zret;
}

# Because File::Spec::updir just adds a '..' to the end.
sub ParentDir {
    my ($path) = @_;
    my @d = File::Spec->splitdir($path);
    my $p;
    # Bizarre but it handles emtpy directories.
    do {
        $p = pop @d;
    } while scalar @d and not $p;
    return File::Spec->catdir(@d);
}

sub parent {
    my ($self) = @_;
    return $self->isTop
        ? undef
        : GetLocation(ParentDir($self->path))
        ;
}

# Add the src to the ordered list of stems.
sub appendSrc {
    my ($self, $src) = @_;
    push @{$self->{_ORDER}}, $src;
}

# Find the source index for a stem.
sub indexOfStem {
    my ($self, $stem) = @_;
    my $zret = 0;
    for my $src (@{$self->{_ORDER}}) {
        if (not ref $src and $src eq $stem) {
            # It's a not yet set directory - who should fault this in?
            return $zret;
        } elsif (ref $src and $stem eq $src->stem) {
            return $zret;
        }
        ++$zret;
    }
    return -1;
}

# Return number of sources.
sub srcCount {
    return scalar @{$_[0]->{_ORDER}};
}

# Return the source at the specified index
sub srcAt {
    my ($self, $n) = @_;
    my $src = $self->{_ORDER}->[$n];
    if (not ref $src) {
        $src = GetLocation(File::Spec->catdir($self->path, $src));
        die "Internal error: Unknown cyclic / invalid directory reference.\n" unless $src;
        $self->{_ORDER}->[$n] = $src;
    }
    return $src;
}

sub LastOf { return $_[$#_]; }

# Return a hash of stems in a directory.
#  stem -> name for directories.
#  stem -> array of file names for content files.
# - Directory path.
sub _LoadStems{
    my ($dir) = @_;
    # Two things to count -- language based files and directories.
    my %stems;
    for (glob(File::Spec->catfile($dir, '*'))) {
        my $path = $_;
        next unless -r $path;
        my $name = (File::Spec->splitpath($path))[2];
        next if '.' eq substr($name, 0, 1);
        if (-d $path) {
            die "Stem '$name' used for both files and directory in '$dir'.\n" if exists $stems{$name};
            $stems{$name} = $name;
        } else {
            my ($stem, $lang, $format) = view::_SplitContentName($name);
            if ($stem) {
                die "'Invalid source format '$format' for '$stem.$lang' in '$dir'\n" unless $view::SUFFIXES{$format};
                my $src = $stems{$stem};
                if (not $src) {
                    $src = $stems{$stem} = view::Source->new($dir, $stem);
                } elsif (not ref $src) {
                    die "Stem '$stem' used for both files and directory in '$dir'.\n";
                }
                $src->setFileForLang($name, $lang);
            }
        }
    }
    return \%stems;
}

# Given a full path to a directory, load it in to a Source or Node as appropriate.
sub _LoadPath {
    my ($path) = @_;
    die "Must only load directories.\n" unless -d $path;
    $path =~ s!/+$!!;
    my $stem = LastOf(File::Spec->splitdir($path));
    my $node = view::Node->new($path, $stem);
    # Scan directory for stems.
    my $stem_map = _LoadStems($path);
    # Check for order file.
    my $order_path= File::Spec->catfile($path, view::ORDER_FILE());
    my @order;
    if (open my $fh, '<', $order_path) {
        local $/; # Grab the entire file
        @order = split(/\s+/, <$fh>);
        close $fh;
        @order = grep { $_ ne view::INDEX_STEM } @order; # index file never allowed.
        # If the index file exists, use that as node content. Otherwise fail.
        if (exists $stem_map->{view::INDEX_STEM()}) {
            $node->copyLangMap($stem_map->{view::INDEX_STEM()});
        } elsif (scalar @order == 1) {
            # No index file, but the order list is a singleton so use that.
            $node->copyLangMap($stem_map->{$order[0]});
            @order = ();
        } else {
            die "An order file was found but the required '", view::INDEX_STEM(), "' stem for directory content was not found in '$path'.\n";
        }
        # Add the order specified stems to the node.
        my $fatal = FALSE; # so we can print all missing stems.
        for my $stem (@order) {
            my $src = $stem_map->{$stem};
            if (not $src) {
                print "Stem '$stem' in order file but not in directory.\n";
                $fatal = TRUE;
            } else {
                # If it's not an object, it's a source stem, locate if
                # available. Otherwise leave it to be faulted in later.
                $src = $Locations{$stem} if not ref $src and ref $Locations{$stem};
                $node->appendSrc($src);
            }
        }
        die "Invalid order file in '$path'.\n" if $fatal;
    } else {
        # If there's no order file then only one stem is allowed and it
        # is treated as the content for this node.
        my @keys = keys %$stem_map;
        if (scalar @keys == 0) {
            die "No valid stems were found in '$path'.\n"
        } elsif (scalar @keys > 1) {
            die "Directories with multiple stems [", join(',', @keys), "] must have an '", view::ORDER_FILE(), "' ordering file at '$path'.\n" unless scalar @keys == 1;
        }
        my $src = $stem_map->{$keys[0]};
        die "Directories cannot contain only a single nested directory at '$path'.\n" unless ref $src and $src->isa('view::Source');
        $node->copyLangMap($src); # copy over the file mapping.
    }
    # See if we've got any variables defined.
    my $var_path = File::Spec->catfile($path, view::VAR_FILE());
    if (-r $var_path) {
        my %vars;
        view::read_text_file $var_path, \%vars;
        delete $vars{content};
        my $top_p = $vars{headers}{top} || $vars{headers}{Top};
        $node->isTop($top_p and $top_p =~ m/true|yes|1/i);
        $node->vars(\%vars);
    }
    return $node;
}

# Get a location, return as a node.
# This will recurse up the directory chain until it hits an already loaded
# node or the top.
# Args:
#  - Directory to retrieve.
sub GetLocation {
    my ($dir) = @_;
    my $node = $Locations{$dir};
    if (not $node) {
        $node = _LoadPath($dir);
        $Locations{$dir} = $node;
        if (not $node->isTop) {
            my $parent = $node->parent; # magic recursion happens here.
            my %locals = %{$node->vars}; # cache local vars.
            $node->vars(Storable::dclone($parent->vars)); # copy parent values.
            # overwrite parent values with locals.
            @{$node->vars->{keys %locals}} = values %locals;
        }
    }
    return $node;
}

# Get a source which is presumed to be a file (not a directory).
# Return the Source object in scalar context
#        the Source object and the language tag in array context.
# Args:
# - Path to file.
sub GetSource {
    my ($path) = @_;
    my ($vol, $dir, $file) = File::Spec->splitpath($path);
    my $node = GetLocation($dir);
    my $src = $node;
    my ($stem, $lang, $ext) = view::_SplitContentName($file);
    if ($node->langToFile($lang) ne $file) {
        my $idx = $node->indexOfStem($stem);
        $src = undef;
        if ($idx >= 0) {
            # If we don't find it in the index, that's tolerable
            # it means the file is there but it's not in order.txt
            # so just ignore it by return undef
            $src = $node->srcAt($idx);
            die "Failed to find source for '$path'.\n" unless $file eq $src->langToFile($lang);
        }
    }
    return wantarray ? ( $src , $lang ) : $src;
}

package view;

#
# BUILD CONSTRAINT:  all views must return $content, $extension.
# additional return values (as seen below) are optional.  However,
# careful use of symlinks and dependency management in path.pm can
# resolve most issues with this constraint.
#

push @Dotiac::DTL::TEMPLATE_DIRS, "templates";

# This is most widely used view.  It takes a
# 'template' argument and a 'path' argument.
# Assuming the path ends in foo.mdtext, any files
# like foo.page/bar.mdtext will be parsed and
# passed to the template in the "bar" (hash)
# variable.

sub single_narrative {
    my %args = @_;
    my $file = SRC_DIR . $args{path};
    my $template = $args{template};
    $args{path} =~ s/\.mdtext$/\.html/;
    $args{breadcrumbs} = breadcrumbs($args{path});
    
    ## Set globals.
    # base URL for the page. Pass it on to the template as well.
    if ($PAGE_BASE = $ENV{ATS_CMS_PAGE_BASE}) {
        $args{page_base} = $PAGE_BASE;
    }
    # base output directory.
    $TARGET_BASE = $ENV{TARGET_BASE};

    my ($src, $lang) = view::Node::GetSource($file);
    if (not $src) {
        warn "Ignoring '$file' because an order.txt was present without this stem.\n";
        return '';
    }
    
    $args{langs} = langs($src, $lang);
    $args{lang} = $lang;
    # copy over file local vars, including content.
    my $fvars = $src->langToVars($lang);
    @args{keys %$fvars} = values %$fvars;
    # Get the nav tree up to date.
    $args{nav_tree} = refresh_nav_tree($src, $lang);
    # Compute page local nav links.
    nav_links($src, \%args) if $args{headers}{navigation};
    
    return $doxy::WV->process(Dotiac::DTL::Template($template)->render(\%args)), html => \%args;
}

# Similar to single_narrative but does no auto navigation.
sub standalone_page {
    my %args = @_;
    my $file = SRC_DIR . $args{path};
    my $template = $args{template};
    $args{path} =~ s/\.mdtext$/\.html/;
    
    ## Set globals.
    # base URL for the page. Pass it on to the template as well.
    if ($PAGE_BASE = $ENV{ATS_CMS_PAGE_BASE}) {
        $args{page_base} = $PAGE_BASE;
    }
    # base output directory.
    $TARGET_BASE = $ENV{TARGET_BASE};

    read_text_file $file, \%args;
    return Dotiac::DTL::Template($template)->render(\%args), html => \%args;
}

# Has the same behavior as the above for foo.page/bar.txt
# files, parsing them into a bar variable for the template.
# Otherwise presumes the template is the path.

sub news_page {
    my %args = @_;
    my $template = "content$args{path}";
    $args{breadcrumbs} = breadcrumbs($args{path});

    my $page_path = $template;
    $page_path =~ s/\.[^.]+$/.page/;
    if (-d $page_path) {
        for my $f (grep -f, glob "$page_path/*.mdtext") {
            $f =~ m!/([^/]+)\.mdtext$! or die "Bad filename: $f\n";
            $args{$1} = {};
            read_text_file $f, $args{$1};
        }
    }

    for ((fetch_doap_url_list())[0..2]) {
        push @{$args{projects}}, parse_doap($_);
    }

    return Dotiac::DTL::Template($template)->render(\%args), html => \%args;
}

sub sitemap {
    my %args = @_;
    my $template = "content$args{path}";
    $args{breadcrumbs} .= breadcrumbs($args{path});
    my $dir = $template;
    $dir =~ s!/[^/]+$!!;
    opendir my $dh, $dir or die "Can't opendir $dir: $!\n";
    my %data;
    for (map "$dir/$_", grep $_ ne "." && $_ ne ".." && $_ ne ".svn", readdir $dh) {
        if (-f and /\.mdtext$/) {
            my $file = $_;
            $file =~ s/^content//;
            no warnings 'once';
            for my $p (@path::patterns) {
                my ($re, $method, $args) = @$p;
                next unless $file =~ $re;
                my $s = view->can($method) or die "Can't locate method: $method\n";
                my ($content, $ext, $vars) = $s->(path => $file, %$args);
                $file =~ s/\.mdtext$/.$ext/;
                $data{$file} = $vars;
                last;
            }
        }
    }

    my $content = "";

    for (sort keys %data) {
        $content .= "- [$data{$_}->{headers}->{title}]($_)\n";
        for my $hdr (grep /^#/, split "\n", $data{$_}->{content}) {
            $hdr =~ /^(#+)\s+([^#]+)?\s+\1\s+\{#([^}]+)\}$/ or next;
            my $level = length $1;
            $level *= 4;
            $content .= " " x $level;
            $content .= "- [$2]($_#$3)\n";
        }
    }
    $args{content} = $content;
    return Dotiac::DTL::Template($template)->render(\%args), html => \%args;
}

sub exports {
    my %args = @_;
    my $template = "content$args{path}";
    $args{breadcrumbs} = breadcrumbs($args{path});

    my $page_path = $template;
    $page_path =~ s/\.[^.]+$/.page/;
    if (-d $page_path) {
        for my $f (grep -f, glob "$page_path/*.mdtext") {
            $f =~ m!/([^/]+)\.mdtext$! or die "Bad filename: $f\n";
            $args{$1} = {};
            read_text_file $f, $args{$1};
        }
        $args{table} = `xsltproc $page_path/eccnmatrix.xsl $page_path/eccnmatrix.xml`;

    }

    return Dotiac::DTL::Template($template)->render(\%args), html => \%args;
}

sub parse_doap {
    my $url = shift;
    my $doap = get $url or die "Can't get $url: $!\n";
    my ($fh, $filename) = tempfile("XXXXXX");
    print $fh $doap;
    close $fh;
    my $result = eval `xsltproc lib/doap2perl.xsl $filename`;
    unlink $filename;
    return $result;
}

sub fetch_doap_url_list {
    my $xml = get "http://svn.apache.org/repos/asf/infrastructure/site-tools/trunk/projects/files.xml"
        or die "Can't get doap file list: $!\n";
    my ($fh, $filename) = tempfile("XXXXXX");
    print $fh $xml;
    close $fh;
    chomp(my @urls = grep /^http/, `xsltproc lib/list2urls.xsl $filename`);
    unlink $filename;
    shuffle \@urls;
return @urls;
}

sub breadcrumbs {
    my @path = split m!/!, shift;
    pop @path;
    my @rv;
    my $relpath = "";
    for (@path) {
        $relpath .= "$_/";
        $_ ||= "Home";
        push @rv, qq(<a href="$relpath">\u$_</a>);
    }
    return join "&nbsp;&raquo&nbsp;", @rv;
}

# --------------

# Generate a navigation subtree starting at this node.
# Also need the language and relative path.
sub nav_sub_tree {
    my ($target, $lang) = @_;
    my $text = '<li>';
    
    $text .= '<a href="';
    $text .= File::Spec->catfile($target->absLinkPath, $target->langToLink($lang));
    $text .= '" >';
    $text .= $target->langToVars($lang)->{headers}{title} || '<strong>Title Missing</strong>';
    $text .= '</a>';
    
    my $n = $target->srcCount;
    if ($n) {
        $text .= '<ul>';
        for (my $i = 0 ; $i < $n ; ++$i) {
            $text .= nav_sub_tree($target->srcAt($i), $lang);
        }
        $text .= '</ul>';
    }
    
    $text .= '</li>';
    return $text;
}

# Compute the full tree containing this node.
sub nav_full_tree {
    my ($node, $lang) = @_;
    return '<ul>' . nav_sub_tree($node->top, $lang) . '</ul>';
}

# Args:
# - Node containing next source.
# - Index of next source (must be in range)
# - Target language.
# - Relative path to node.
sub next_link {
    my ($node, $idx, $lang, $rpath) = @_;
    my $other = $node->srcAt($idx);
    my $url = '';
    $url = _append_to_path($rpath, $other->stem) if $other->isa('view::Node');
    $url = _append_to_path($url, $other->langToLink($lang));
    return $url;    
}

# Return relative path to the rightmost leaf.
#  - Starting source.
#  - Stem in source to start.
#  - Language
#  - Relative path to start.
sub right_leaf {
    my ($node, $stem, $lang, $rpath) = @_;
    my $src = $node->srcAt($node->indexOfStem($stem));
    while (1) {
        if ($src->srcCount > 0) {
            # src is a directory with multiple nodes, descend in to it.
            $rpath = _append_to_path($rpath, $src->stem);
            $node = $src;
            $src = $node->srcAt($node->srcCount - 1);
        } else { # we've hit the bottom, finish.
            $rpath = _append_to_path($rpath, $stem) if $src->isa('view::Node');
            $rpath = $rpath ? File::Spec->catfile($rpath, $src->langToLink($lang)) : $src->langToLink($lang);
            last;
        }
    }
    return $rpath;
}

# Args - Directory chain.
# Return an array that contains navigation bar data.
sub auto_nav {
    my ($src, $lang) = @_;
    my @nav; # return: Array of nav items
    my $prefix; # path relative to source content file.
    my $parent = $src->parent;
    
    # Flags for next and back links.
    # We don't insert directly because we need to force a specific ordering.
    my $next_url;
    my $back_url;

    # Back URL is always nearby because of the node/content asymmetry so
    # just compute it.
    if ($parent) { # There's no back from the top.
        my $bpath = $src->isa('view::Node') ? '..' : '';
        my $idx = $parent->indexOfStem($src->stem);
        if (0 == $idx) { # back from first stem is parent content
            $back_url = _append_to_path($bpath, $parent->langToLink($lang));
        } else {
            $back_url = right_leaf($parent, $parent->srcAt($idx-1)->stem, $lang, $bpath);
        }
    }
    # Special case a next link from the content file of a node. Handling it
    # here avoids ugly '..' handling later.
    if ($src->srcCount > 0) {
        $next_url = next_link($src, 0, $lang, '');
    }
    # If we're at a node we will need back up the tree. We just took
    # care of the only exception (next link to stem in the same node).
    my $rpath;
    $rpath = '..' if $src->isa('view::Node');
    # Now walk up. If we haven't already done the next link, wait until we
    # can go right and compute the link there. If that never happens, we were
    # at the very last source and no next is what we want. Otherwise we need
    # to walk up to get the relative path to Top.
    while ($parent) {
        if (not $next_url) {
            my $idx = $parent->indexOfStem($src->stem);
            my $n = $parent->srcCount;
            if ($idx < $n - 1 ) {
                $next_url = next_link($parent, $idx+1, $lang, $rpath);
            }
        }
        last if $parent->isTop;
        $rpath = _append_to_path($rpath, '..');
        $src = $parent;
        $parent = $src->parent;
    }

    # Put in the nav links, in correct order.    
    push (@nav, { url => $next_url, text => 'Next' }) if $next_url;
    push (@nav, { url => $back_url, text => 'Back' }) if $back_url;
    push (@nav, { url => File::Spec->catfile($rpath, $parent->langToLink($lang)), text => 'Top' }) if $parent && $src != $parent;

    return \@nav;
}

# Parse a list of links in Markdown format.
sub parse_links {
    my ($input) = @_;
    my @links; # Return: list of link items.
    my $need_fill = 0; # URLs to be named later found.
    for (split(/\s+/, $input)) {
        if (/\[([^\]]+)\]\(([^)]+)/) {
            my ($text, $url) = ($1, $2);
            push (@links, { text => $text, url => $url });
            $need_fill = 1 if $url eq '*';
        }
    }
    return ( \@links , $need_fill );
}

# Generate the nav link data.
# Arguments
#   - Source
#   - 
sub nav_links {
    my ($src, $args) = @_;
    my $lang = $args->{lang};
    
    return if not ref $src;

    # Imp Note: DOTIAC is broken and treats an empty array as "true"
    # when it should be "false". So we must avoid setting the nav link
    # arg if the list is empty.

    # Process any user supplied nav links.
    my $explicit; # array ref of nav links from user data.
    my $fill = 0; # Need to fill generated links from navigation?
    if ($args->{headers}{navigation}) {
        ($explicit, $fill) = parse_links($args->{headers}{navigation});
    }
    if ($explicit) {
        # If user has marked nav links for fill from generated data,
        # find those and fill in the generated values.
        if ($fill) {
            # Get the auto navs and then make a hash keyed by text.
            my $auto_list = auto_nav($src, $lang);
            my %auto_map = map { $_->{text} => $_ } @$auto_list;
            # Do the direct fills
            for (@$explicit) {
                if (exists $auto_map{$_->{text}} and $_->{url} eq '*') {
                    my $tag = $_->{text};
                    $_->{url} = $auto_map{$tag}->{url};
                    $auto_map{$tag}->{used} = 1;
                }
            }
            # Do it again and do the generic fill, if any.
            # Need to do generic fill separately so we don't duplicate.
            my @unused = grep {
                not exists $auto_map{$_->{text}}{used}
            } @$auto_list;

            @$explicit = map {
            $_->{text} eq '*' && $_->{url} eq '*' ? @unused : $_;
            } @$explicit if @unused;

            # Clean out any navs that were marked for substitution but
            # didn't get matched.
            @$explicit = grep { $_->{url} ne '*' } @$explicit;
        }
        $args->{nav} = $explicit if @$explicit;
    } else {
        my $auto_navs = auto_nav($src, $lang);
        $args->{nav} = $auto_navs if @$auto_navs;
    }
}

# Arg: Target file path.
# Return: List of link objects.
sub langs {
    my ($src, $lang) = @_;
    my @langs; # Return: List of link objects.
    for my $ol ($src->langList) {    
        next if $ol eq $lang; # don't include us.
        push(@langs, { text=>$ol, url=>$src->langToFile($ol) } );
    }
    # Django support is broken, empty arrays are true.
    return @langs ? \@langs : undef;
}

# Refresh the nav tree file if stale (older than source file)
# Return the relative path to the nav tree file.
sub refresh_nav_tree {
    my ($src, $lang) = @_;
    my $src_path = File::Spec->catfile($src->path, $src->langToFile($lang));
    my $top = $src->top;
    my $top_path = File::Spec->catfile($top->path, $top->langToFile($lang));
    my $nav_file = $lang . '-' . NAV_TREE_FILE;
    # If we have TARGET_BASE, generate the file there, otherwise make it locally
    # and count on it being copied over with everything else.
    my $nav_path = $top->path;
    $nav_path = File::Spec->catdir($TARGET_BASE, $nav_path) if $TARGET_BASE;
    $nav_path = File::Spec->catfile($nav_path, $nav_file);
    # If the nav file is missing or older than this source file, write it.
    if (! -r $nav_path or -M $nav_path > -M $src_path) {
        if (open my $fh, '>', $nav_path) {
            my $text = nav_full_tree($top, $lang);
            print $fh $text;
            close $fh;
        } else {
            die "Unable to open $nav_path for writing: $@\n";
        }
    }
    my $rpath = $src->pathTo($top);
    return $rpath ? File::Spec->catfile($rpath, $nav_file) : $nav_file;
}

=head1 LICENSE

           Licensed to the Apache Software Foundation (ASF) under one
           or more contributor license agreements.  See the NOTICE file
           distributed with this work for additional information
           regarding copyright ownership.  The ASF licenses this file
           to you under the Apache License, Version 2.0 (the
           "License"); you may not use this file except in compliance
           with the License.  You may obtain a copy of the License at

             http://www.apache.org/licenses/LICENSE-2.0

           Unless required by applicable law or agreed to in writing,
           software distributed under the License is distributed on an
           "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
           KIND, either express or implied.  See the License for the
           specific language governing permissions and limitations
           under the License.
