package org.sonatype.aether.test.util;

/*******************************************************************************
 * Copyright (c) 2010-2011 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 *   http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.sonatype.aether.artifact.Artifact;
import org.sonatype.aether.graph.Dependency;
import org.sonatype.aether.graph.DependencyNode;

/**
 * Creates a dependency tree from a text description. <h2>Definition</h2> The description format is based on 'mvn
 * dependency:tree'. Line format:
 * 
 * <pre>
 * [level]dependencyDefinition[;key=value;key=value;...]
 * </pre>
 * 
 * A <code>dependencyDefinition</code> is of the form:
 * 
 * <pre>
 * (id|[(id)]gid:aid:ext:ver[:scope])
 * </pre>
 * 
 * It may also be <code>(null)</code> to indicate an "empty" node with no dependency.
 * <p>
 * <h2>Levels</h2>
 * <p>
 * If <code>level</code> is empty, the line defines the root node. Only one root node may be defined. The level is
 * calculated by the distance from the beginning of the line. One level is three characters. A level definition has to
 * follow this format:
 * 
 * <pre>
 * '[| ]*[+\\]- '
 * </pre>
 * 
 * <h2>ID</h2> An ID may be used to reference a previously built node. An ID is of the form:
 * 
 * <pre>
 * '[0-9a-zA-Z]+'
 * </pre>
 * 
 * To define a node with an ID, prefix the definition with an id in parens:
 * 
 * <pre>
 * (id)gid:aid:ext:ver
 * </pre>
 * 
 * To insert a previously defined node into the graph, use a caret followed by the ID:
 * 
 * <pre>
 * ^id
 * </pre>
 * 
 * <h2>Comments</h2>
 * <p>
 * A hash starts a comment. A comment ends with the end of the line. Empty lines are ignored.
 * <h2>Example</h2>
 * 
 * <pre>
 * gid:aid:ext:ver
 * +- gid:aid2:ext:ver:scope
 * |  \- (id1)gid:aid3:ext:ver
 * +- gid:aid4:ext:ver:scope
 * \- ^id1
 * </pre>
 * 
 * <h2>Multiple definitions in one resource</h2>
 * <p>
 * By using {@link #parseMultiple(String)}, definitions divided by a line beginning with "---" can be read from the same
 * resource. The rest of the line is ignored.
 * <h2>Substitutions</h2>
 * <p>
 * You may define substitutions (see {@link #setSubstitutions(String...)},
 * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next
 * String in the defined substitutions.
 * <h3>Example</h3>
 * 
 * <pre>
 * parser.setSubstitutions( &quot;foo&quot;, &quot;bar&quot; );
 * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
 * </pre>
 * 
 * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its
 * artifact id.
 * 
 * @author Benjamin Hanzelmann
 */
public class DependencyGraphParser
{
    private Map<String, DependencyNode> nodes = new HashMap<String, DependencyNode>();

    private String prefix = "";

    private Collection<String> substitutions;

    private Iterator<String> substitutionIterator;

    /**
     * Parse the given graph definition.
     */
    public DependencyNode parseLiteral( String dependencyGraph )
        throws IOException
    {
        BufferedReader reader = new BufferedReader( new StringReader( dependencyGraph ) );
        DependencyNode node = parse( reader );
        reader.close();
        return node;
    }

    /**
     * Create a parser with the given prefix and the given substitution strings.
     * 
     * @see DependencyGraphParser#parse(String)
     */
    public DependencyGraphParser( String prefix, Collection<String> substitutions )
    {
        this.prefix = prefix;
        this.substitutions = substitutions;
    }

    /**
     * Create a parser with the given prefix.
     * 
     * @see DependencyGraphParser#parse(String)
     */
    public DependencyGraphParser( String prefix )
    {
        this( prefix, null );
    }

    /**
     * Create a parser with an empty prefix.
     */
    public DependencyGraphParser()
    {
        this( "" );
    }

    /**
     * Parse the graph definition read from the given resource. If a prefix is set, this method will load the resource
     * from 'prefix + resource'.
     */
    public DependencyNode parse( String resource )
        throws IOException
    {
        URL res = this.getClass().getClassLoader().getResource( prefix + resource );
        if ( res == null )
        {
            throw new IOException( "Could not find classpath resource " + prefix + resource );
        }
        return parse( res );
    }

    /**
     * Parse multiple graphs in one resource, divided by "---".
     */
    public List<DependencyNode> parseMultiple( String resource )
        throws IOException
    {
        URL res = this.getClass().getClassLoader().getResource( prefix + resource );
        if ( res == null )
        {
            throw new IOException( "Could not find classpath resource " + prefix + resource );
        }

        BufferedReader reader = new BufferedReader( new InputStreamReader( res.openStream(), "UTF-8" ) );

        List<DependencyNode> ret = new ArrayList<DependencyNode>();
        DependencyNode root = null;
        while ( ( root = parse( reader ) ) != null )
        {
            ret.add( root );
        }
        return ret;
    }

    /**
     * Parse the graph definition read from the given URL.
     */
    public DependencyNode parse( URL resource )
        throws IOException
    {
        InputStream stream = null;
        try
        {
            stream = resource.openStream();
            return parse( new BufferedReader( new InputStreamReader( stream, "UTF-8" ) ) );
        }
        finally
        {
            if ( stream != null )
            {
                stream.close();
            }
        }
    }

    private DependencyNode parse( BufferedReader in )
        throws IOException
    {

        if ( substitutions != null )
        {
            substitutionIterator = substitutions.iterator();
        }

        String line = null;

        DependencyNode root = null;
        DependencyNode node = null;
        int prevLevel = 0;

        LinkedList<DependencyNode> stack = new LinkedList<DependencyNode>();
        boolean isRootNode = true;

        while ( ( line = in.readLine() ) != null )
        {
            line = cutComment( line );

            if ( isEmpty( line ) )
            {
                // skip empty line
                continue;
            }

            if ( isEOFMarker( line ) )
            {
                // stop parsing
                break;
            }

            while ( line.contains( "%s" ) )
            {
                if ( !substitutionIterator.hasNext() )
                {
                    throw new IllegalArgumentException( "not enough substitutions to fill placeholders" );
                }
                line = line.replaceFirst( "%s", substitutionIterator.next() );
            }

            LineContext ctx = createContext( line );
            if ( prevLevel < ctx.getLevel() )
            {
                // previous node is new parent
                stack.add( node );
            }

            // get to real parent
            while ( prevLevel > ctx.getLevel() )
            {
                stack.removeLast();
                prevLevel -= 1;
            }

            prevLevel = ctx.getLevel();

            if ( ctx.getDefinition() != null && ctx.getDefinition().isReference() )
            {
                DependencyNode child = reference( ctx.getDefinition().getReference() );
                node.getChildren().add( child );
                node = child;
            }
            else
            {

                node = build( isRootNode ? null : stack.getLast(), ctx, isRootNode );

                if ( isRootNode )
                {
                    root = node;
                    isRootNode = false;
                }

                if ( ctx.getDefinition() != null && ctx.getDefinition().hasId() )
                {
                    this.nodes.put( ctx.getDefinition().getId(), node );
                }
            }
        }

        this.nodes.clear();

        return root;

    }

    private boolean isEOFMarker( String line )
    {
        return line.startsWith( "---" );
    }

    private DependencyNode reference( String reference )
    {
        if ( !nodes.containsKey( reference ) )
        {
            throw new IllegalArgumentException( "undefined reference " + reference );
        }

        return this.nodes.get( reference );
    }

    private static boolean isEmpty( String line )
    {
        return line == null || line.length() == 0;
    }

    private static String cutComment( String line )
    {
        int idx = line.indexOf( '#' );

        if ( idx != -1 )
        {
            line = line.substring( 0, idx );
        }

        return line;
    }

    private DependencyNode build( DependencyNode parent, LineContext ctx, boolean isRoot )
    {
        ArtifactDefinition def = ctx.getDefinition();
        if ( !isRoot && parent == null )
        {
            throw new IllegalArgumentException( "dangling node: " + def );
        }
        else if ( ctx.getLevel() == 0 && parent != null )
        {
            throw new IllegalArgumentException( "inconsistent leveling (parent for level 0?): " + def );
        }

        NodeBuilder builder = new NodeBuilder();

        if ( def != null )
        {
            builder.artifactId( def.getArtifactId() ).groupId( def.getGroupId() );
            builder.ext( def.getExtension() ).version( def.getVersion() ).scope( def.getScope() );
            builder.properties( ctx.getProperties() );
        }
        DependencyNode node = builder.build();

        if ( parent != null )
        {
            parent.getChildren().add( node );
        }

        return node;
    }

    public String dump( DependencyNode root )
    {
        StringBuilder ret = new StringBuilder();

        List<NodeEntry> entries = new ArrayList<NodeEntry>();

        addNode( root, 0, entries );

        for ( NodeEntry nodeEntry : entries )
        {
            char[] level = new char[( nodeEntry.getLevel() * 3 )];
            Arrays.fill( level, ' ' );

            if ( level.length != 0 )
            {
                level[level.length - 3] = '+';
                level[level.length - 2] = '-';
            }

            String definition = nodeEntry.getDefinition();

            ret.append( level ).append( definition ).append( "\n" );
        }

        return ret.toString();

    }

    private void addNode( DependencyNode root, int level, List<NodeEntry> entries )
    {

        NodeEntry entry = new NodeEntry();
        Dependency dependency = root.getDependency();
        StringBuilder defBuilder = new StringBuilder();
        if ( dependency == null )
        {
            defBuilder.append( "(null)" );
        }
        else
        {
            Artifact artifact = dependency.getArtifact();

            defBuilder.append( artifact.getGroupId() ).append( ":" ).append( artifact.getArtifactId() ).append( ":" ).append( artifact.getExtension() ).append( ":" ).append( artifact.getVersion() );
            if ( dependency.getScope() != null && ( !"".equals( dependency.getScope() ) ) )
            {
                defBuilder.append( ":" ).append( dependency.getScope() );
            }

            Map<String, String> properties = artifact.getProperties();
            if ( !( properties == null || properties.isEmpty() ) )
            {
                for ( Map.Entry<String, String> prop : properties.entrySet() )
                {
                    defBuilder.append( ";" ).append( prop.getKey() ).append( "=" ).append( prop.getValue() );
                }
            }
        }

        entry.setDefinition( defBuilder.toString() );
        entry.setLevel( level++ );

        entries.add( entry );

        for ( DependencyNode node : root.getChildren() )
        {
            addNode( node, level, entries );
        }

    }

    class NodeEntry
    {
        int level;

        String definition;

        Map<String, String> properties;

        public int getLevel()
        {
            return level;
        }

        public void setLevel( int level )
        {
            this.level = level;
        }

        public String getDefinition()
        {
            return definition;
        }

        public void setDefinition( String definition )
        {
            this.definition = definition;
        }

        public Map<String, String> getProperties()
        {
            return properties;
        }

        public void setProperties( Map<String, String> properties )
        {
            this.properties = properties;
        }
    }

    private static LineContext createContext( String line )
    {
        LineContext ctx = new LineContext();
        String definition;

        String[] split = line.split( "- " );
        if ( split.length == 1 ) // root
        {
            ctx.setLevel( 0 );
            definition = split[0];
        }
        else
        {
            ctx.setLevel( (int) Math.ceil( (double) split[0].length() / (double) 3 ) );
            definition = split[1];
        }

        if ( "(null)".equalsIgnoreCase( definition ) )
        {
            return ctx;
        }

        split = definition.split( ";" );
        ctx.setDefinition( new ArtifactDefinition( split[0] ) );

        if ( split.length > 1 ) // properties
        {
            Map<String, String> props = new HashMap<String, String>();
            for ( int i = 1; i < split.length; i++ )
            {
                String[] keyValue = split[i].split( "=" );
                String key = keyValue[0];
                String value = keyValue[1];
                props.put( key, value );
            }
            ctx.setProperties( props );
        }

        return ctx;
    }

    static class LineContext
    {
        ArtifactDefinition definition;

        private Map<String, String> properties;

        int level;

        public ArtifactDefinition getDefinition()
        {
            return definition;
        }

        public void setDefinition( ArtifactDefinition definition )
        {
            this.definition = definition;
        }

        public Map<String, String> getProperties()
        {
            return properties;
        }

        public void setProperties( Map<String, String> properties )
        {
            this.properties = properties;
        }

        public int getLevel()
        {
            return level;
        }

        public void setLevel( int level )
        {
            this.level = level;
        }
    }

    public Collection<String> getSubstitutions()
    {
        return substitutions;
    }

    public void setSubstitutions( Collection<String> substitutions )
    {
        this.substitutions = substitutions;
    }

    public void setSubstitutions( String... substitutions )
    {
        this.setSubstitutions( Arrays.asList( substitutions ) );

    }

}
